Add Docker Compose support
Add `spring-boot-docker-compose` module with service connection support. Closes gh-34747 Co-authored-by: Phillip Webb <pwebb@vmware.com> Co-authored-by: "Andy Wilkinson <wilkinsona@vmware.com>pull/35031/head
parent
4ae24e404e
commit
842e17eced
@ -0,0 +1,32 @@
|
|||||||
|
plugins {
|
||||||
|
id "java-library"
|
||||||
|
id "org.springframework.boot.configuration-properties"
|
||||||
|
id "org.springframework.boot.conventions"
|
||||||
|
id "org.springframework.boot.deployed"
|
||||||
|
id "org.springframework.boot.optional-dependencies"
|
||||||
|
}
|
||||||
|
|
||||||
|
description = "Spring Boot Docker Compose Support"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api(project(":spring-boot-project:spring-boot"))
|
||||||
|
|
||||||
|
implementation("com.fasterxml.jackson.core:jackson-databind")
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-parameter-names")
|
||||||
|
|
||||||
|
optional(project(":spring-boot-project:spring-boot-autoconfigure"))
|
||||||
|
optional(project(":spring-boot-project:spring-boot-actuator-autoconfigure"))
|
||||||
|
optional("io.r2dbc:r2dbc-spi")
|
||||||
|
optional("org.mongodb:mongodb-driver-core")
|
||||||
|
optional("org.springframework.data:spring-data-r2dbc")
|
||||||
|
|
||||||
|
|
||||||
|
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
|
||||||
|
testImplementation(project(":spring-boot-project:spring-boot-test"))
|
||||||
|
testImplementation("org.springframework:spring-core-test")
|
||||||
|
testImplementation("org.springframework:spring-test")
|
||||||
|
testImplementation("org.assertj:assertj-core")
|
||||||
|
testImplementation("org.mockito:mockito-core")
|
||||||
|
testImplementation("ch.qos.logback:logback-classic")
|
||||||
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides access to the ports that can be used to connect to a {@link RunningService}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
* @see RunningService
|
||||||
|
*/
|
||||||
|
public interface ConnectionPorts {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the host port mapped to the given container port.
|
||||||
|
* @param containerPort the container port. This is usually the standard port for the
|
||||||
|
* service (e.g. port 80 for HTTP)
|
||||||
|
* @return the host port. This can be an ephemeral port that is different from the
|
||||||
|
* container port
|
||||||
|
* @throws IllegalStateException if the container port is not mapped
|
||||||
|
*/
|
||||||
|
int get(int containerPort);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all host ports in use.
|
||||||
|
* @return a list of all host ports
|
||||||
|
* @see #getAll(String)
|
||||||
|
*/
|
||||||
|
List<Integer> getAll();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all host ports in use that match the given protocol.
|
||||||
|
* @param protocol the protocol in use (for example 'tcp') or {@code null} to return
|
||||||
|
* all host ports
|
||||||
|
* @return a list of all host ports using the given protocol
|
||||||
|
*/
|
||||||
|
List<Integer> getAll(String protocol);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.Config;
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostConfig;
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostPort;
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.NetworkSettings;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default {@link ConnectionPorts} implementation backed by {@link DockerCli} responses.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DefaultConnectionPorts implements ConnectionPorts {
|
||||||
|
|
||||||
|
private Map<ContainerPort, Integer> mappings = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
private Map<Integer, Integer> portMappings = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
DefaultConnectionPorts(DockerCliInspectResponse inspectResponse) {
|
||||||
|
this.mappings = !isHostNetworkMode(inspectResponse)
|
||||||
|
? buildMappingsForNetworkSettings(inspectResponse.networkSettings())
|
||||||
|
: buildMappingsForHostNetworking(inspectResponse.config());
|
||||||
|
Map<Integer, Integer> portMappings = new HashMap<>();
|
||||||
|
this.mappings.forEach((containerPort, hostPort) -> portMappings.put(containerPort.number(), hostPort));
|
||||||
|
this.portMappings = Collections.unmodifiableMap(portMappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isHostNetworkMode(DockerCliInspectResponse inspectResponse) {
|
||||||
|
HostConfig config = inspectResponse.hostConfig();
|
||||||
|
return (config != null) && "host".equals(config.networkMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<ContainerPort, Integer> buildMappingsForNetworkSettings(NetworkSettings networkSettings) {
|
||||||
|
if (networkSettings == null || CollectionUtils.isEmpty(networkSettings.ports())) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
Map<ContainerPort, Integer> mappings = new HashMap<>();
|
||||||
|
networkSettings.ports().forEach((containerPortString, hostPorts) -> {
|
||||||
|
if (!CollectionUtils.isEmpty(hostPorts)) {
|
||||||
|
ContainerPort containerPort = ContainerPort.parse(containerPortString);
|
||||||
|
hostPorts.stream()
|
||||||
|
.filter(this::isIpV4)
|
||||||
|
.forEach((hostPort) -> mappings.put(containerPort, getPortNumber(hostPort)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Collections.unmodifiableMap(mappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isIpV4(HostPort hostPort) {
|
||||||
|
String ip = (hostPort != null) ? hostPort.hostIp() : null;
|
||||||
|
return !StringUtils.hasLength(ip) || ip.contains(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getPortNumber(HostPort hostPort) {
|
||||||
|
return Integer.parseInt(hostPort.hostPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<ContainerPort, Integer> buildMappingsForHostNetworking(Config config) {
|
||||||
|
if (CollectionUtils.isEmpty(config.exposedPorts())) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
Map<ContainerPort, Integer> mappings = new HashMap<>();
|
||||||
|
for (String entry : config.exposedPorts().keySet()) {
|
||||||
|
ContainerPort containerPort = ContainerPort.parse(entry);
|
||||||
|
mappings.put(containerPort, containerPort.number());
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableMap(mappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int get(int containerPort) {
|
||||||
|
Integer hostPort = this.portMappings.get(containerPort);
|
||||||
|
Assert.state(hostPort != null, "No host port mapping found for container port %s".formatted(containerPort));
|
||||||
|
return hostPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Integer> getAll() {
|
||||||
|
return getAll(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Integer> getAll(String protocol) {
|
||||||
|
List<Integer> hostPorts = new ArrayList<>();
|
||||||
|
this.mappings.forEach((containerPort, hostPort) -> {
|
||||||
|
if (protocol == null || protocol.equalsIgnoreCase(containerPort.protocol())) {
|
||||||
|
hostPorts.add(hostPort);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Collections.unmodifiableList(hostPorts);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<ContainerPort, Integer> getMappings() {
|
||||||
|
return this.mappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container port consisting of a number and protocol.
|
||||||
|
*
|
||||||
|
* @param number the port number
|
||||||
|
* @param protocol the protocol (e.g. tcp)
|
||||||
|
*/
|
||||||
|
static record ContainerPort(int number, String protocol) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "%d/%s".formatted(this.number, this.protocol);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ContainerPort parse(String value) {
|
||||||
|
try {
|
||||||
|
String[] parts = value.split("/");
|
||||||
|
Assert.state(parts.length == 2, "Unable to split string");
|
||||||
|
return new ContainerPort(Integer.parseInt(parts[0]), parts[1]);
|
||||||
|
}
|
||||||
|
catch (RuntimeException ex) {
|
||||||
|
throw new IllegalStateException("Unable to parse container port '%s'".formatted(value), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default {@link DockerCompose} implementation backed by {@link DockerCli}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DefaultDockerCompose implements DockerCompose {
|
||||||
|
|
||||||
|
private final DockerCli cli;
|
||||||
|
|
||||||
|
private final DockerHost hostname;
|
||||||
|
|
||||||
|
DefaultDockerCompose(DockerCli cli, String host) {
|
||||||
|
this.cli = cli;
|
||||||
|
this.hostname = DockerHost.get(host, () -> cli.run(new DockerCliCommand.Context()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void up() {
|
||||||
|
this.cli.run(new DockerCliCommand.ComposeUp());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void down(Duration timeout) {
|
||||||
|
this.cli.run(new DockerCliCommand.ComposeDown(timeout));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
this.cli.run(new DockerCliCommand.ComposeStart());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop(Duration timeout) {
|
||||||
|
this.cli.run(new DockerCliCommand.ComposeStop(timeout));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasDefinedServices() {
|
||||||
|
return !this.cli.run(new DockerCliCommand.ComposeConfig()).services().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasRunningServices() {
|
||||||
|
return runComposePs().stream().anyMatch(this::isRunning);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<RunningService> getRunningServices() {
|
||||||
|
List<DockerCliComposePsResponse> runningPsResponses = runComposePs().stream().filter(this::isRunning).toList();
|
||||||
|
if (runningPsResponses.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
DockerComposeFile dockerComposeFile = this.cli.getDockerComposeFile();
|
||||||
|
List<RunningService> result = new ArrayList<>();
|
||||||
|
Map<String, DockerCliInspectResponse> inspected = inspect(runningPsResponses);
|
||||||
|
for (DockerCliComposePsResponse psResponse : runningPsResponses) {
|
||||||
|
DockerCliInspectResponse inspectResponse = inspected.get(psResponse.id());
|
||||||
|
result.add(new DefaultRunningService(this.hostname, dockerComposeFile, psResponse, inspectResponse));
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, DockerCliInspectResponse> inspect(List<DockerCliComposePsResponse> runningPsResponses) {
|
||||||
|
List<String> ids = runningPsResponses.stream().map(DockerCliComposePsResponse::id).toList();
|
||||||
|
List<DockerCliInspectResponse> inspectResponses = this.cli.run(new DockerCliCommand.Inspect(ids));
|
||||||
|
return inspectResponses.stream().collect(Collectors.toMap(DockerCliInspectResponse::id, Function.identity()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DockerCliComposePsResponse> runComposePs() {
|
||||||
|
return this.cli.run(new DockerCliCommand.ComposePs());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRunning(DockerCliComposePsResponse psResponse) {
|
||||||
|
return !"exited".equals(psResponse.state());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.boot.origin.Origin;
|
||||||
|
import org.springframework.boot.origin.OriginProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default {@link RunningService} implementation backed by {@link DockerCli} responses.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DefaultRunningService implements RunningService, OriginProvider {
|
||||||
|
|
||||||
|
private final Origin origin;
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private final ImageReference image;
|
||||||
|
|
||||||
|
private final DockerHost host;
|
||||||
|
|
||||||
|
private final DefaultConnectionPorts ports;
|
||||||
|
|
||||||
|
private final Map<String, String> labels;
|
||||||
|
|
||||||
|
private DockerEnv env;
|
||||||
|
|
||||||
|
DefaultRunningService(DockerHost host, DockerComposeFile composeFile, DockerCliComposePsResponse psResponse,
|
||||||
|
DockerCliInspectResponse inspectResponse) {
|
||||||
|
this.origin = new DockerComposeOrigin(composeFile, psResponse.name());
|
||||||
|
this.name = psResponse.name();
|
||||||
|
this.image = ImageReference.of(psResponse.image());
|
||||||
|
this.host = host;
|
||||||
|
this.ports = new DefaultConnectionPorts(inspectResponse);
|
||||||
|
this.env = new DockerEnv(inspectResponse.config().env());
|
||||||
|
this.labels = Collections.unmodifiableMap(inspectResponse.config().labels());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Origin getOrigin() {
|
||||||
|
return this.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ImageReference image() {
|
||||||
|
return this.image;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String host() {
|
||||||
|
return this.host.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConnectionPorts ports() {
|
||||||
|
return this.ports;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> env() {
|
||||||
|
return this.env.asMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> labels() {
|
||||||
|
return this.labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliCommand.Type;
|
||||||
|
import org.springframework.core.log.LogMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around {@code docker} and {@code docker-compose} command line tools.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DockerCli {
|
||||||
|
|
||||||
|
private final Log logger = LogFactory.getLog(DockerCli.class);
|
||||||
|
|
||||||
|
private final ProcessRunner processRunner;
|
||||||
|
|
||||||
|
private final List<String> dockerCommand;
|
||||||
|
|
||||||
|
private final List<String> dockerComposeCommand;
|
||||||
|
|
||||||
|
private final DockerComposeFile composeFile;
|
||||||
|
|
||||||
|
private final Set<String> activeProfiles;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link DockerCli} instance.
|
||||||
|
* @param workingDirectory the working directory or {@code null}
|
||||||
|
* @param composeFile the docker compose file to use
|
||||||
|
* @param activeProfiles the docker compose profiles to activate
|
||||||
|
*/
|
||||||
|
DockerCli(File workingDirectory, DockerComposeFile composeFile, Set<String> activeProfiles) {
|
||||||
|
this.processRunner = new ProcessRunner(workingDirectory);
|
||||||
|
this.dockerCommand = getDockerCommand(this.processRunner);
|
||||||
|
this.dockerComposeCommand = getDockerComposeCommand(this.processRunner);
|
||||||
|
this.composeFile = composeFile;
|
||||||
|
this.activeProfiles = (activeProfiles != null) ? activeProfiles : Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getDockerCommand(ProcessRunner processRunner) {
|
||||||
|
try {
|
||||||
|
String version = processRunner.run("docker", "version", "--format", "{{.Client.Version}}");
|
||||||
|
this.logger.trace(LogMessage.format("Using docker %s", version));
|
||||||
|
return List.of("docker");
|
||||||
|
}
|
||||||
|
catch (ProcessStartException ex) {
|
||||||
|
throw new DockerProcessStartException("Unable to start docker process. Is docker correctly installed?", ex);
|
||||||
|
}
|
||||||
|
catch (ProcessExitException ex) {
|
||||||
|
if (ex.getStdErr().contains("docker daemon is not running")
|
||||||
|
|| ex.getStdErr().contains("Cannot connect to the Docker daemon")) {
|
||||||
|
throw new DockerNotRunningException(ex.getStdErr(), ex);
|
||||||
|
}
|
||||||
|
throw ex;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getDockerComposeCommand(ProcessRunner processRunner) {
|
||||||
|
try {
|
||||||
|
DockerCliComposeVersionResponse response = DockerJson.deserialize(
|
||||||
|
processRunner.run("docker", "compose", "version", "--format", "json"),
|
||||||
|
DockerCliComposeVersionResponse.class);
|
||||||
|
this.logger.trace(LogMessage.format("Using docker compose $s", response.version()));
|
||||||
|
return List.of("docker", "compose");
|
||||||
|
}
|
||||||
|
catch (ProcessExitException ex) {
|
||||||
|
// Ignore and try docker-compose
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
DockerCliComposeVersionResponse response = DockerJson.deserialize(
|
||||||
|
processRunner.run("docker-compose", "version", "--format", "json"),
|
||||||
|
DockerCliComposeVersionResponse.class);
|
||||||
|
this.logger.trace(LogMessage.format("Using docker-compose $s", response.version()));
|
||||||
|
return List.of("docker-compose");
|
||||||
|
}
|
||||||
|
catch (ProcessStartException ex) {
|
||||||
|
throw new DockerProcessStartException(
|
||||||
|
"Unable to start 'docker-compose' process or use 'docker compose'. Is docker correctly installed?",
|
||||||
|
ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the given {@link DockerCli} command and return the response.
|
||||||
|
* @param <R> the response type
|
||||||
|
* @param dockerCommand the command to run
|
||||||
|
* @return the response
|
||||||
|
*/
|
||||||
|
<R> R run(DockerCliCommand<R> dockerCommand) {
|
||||||
|
List<String> command = createCommand(dockerCommand.getType());
|
||||||
|
command.addAll(dockerCommand.getCommand());
|
||||||
|
String json = this.processRunner.run(command.toArray(new String[0]));
|
||||||
|
return dockerCommand.deserialize(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <R> List<String> createCommand(Type type) {
|
||||||
|
return switch (type) {
|
||||||
|
case DOCKER -> new ArrayList<>(this.dockerCommand);
|
||||||
|
case DOCKER_COMPOSE -> {
|
||||||
|
List<String> result = new ArrayList<>(this.dockerComposeCommand);
|
||||||
|
if (this.composeFile != null) {
|
||||||
|
result.add("--file");
|
||||||
|
result.add(this.composeFile.toString());
|
||||||
|
}
|
||||||
|
result.add("--ansi");
|
||||||
|
result.add("never");
|
||||||
|
for (String profile : this.activeProfiles) {
|
||||||
|
result.add("--profile");
|
||||||
|
result.add(profile);
|
||||||
|
}
|
||||||
|
yield result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the {@link DockerComposeFile} being used by this CLI instance.
|
||||||
|
* @return the docker compose file
|
||||||
|
*/
|
||||||
|
DockerComposeFile getDockerComposeFile() {
|
||||||
|
return this.composeFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,207 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commands that can be executed by the {@link DockerCli}.
|
||||||
|
*
|
||||||
|
* @param <R> the response type
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
abstract sealed class DockerCliCommand<R> {
|
||||||
|
|
||||||
|
private final Type type;
|
||||||
|
|
||||||
|
private final Class<?> responseType;
|
||||||
|
|
||||||
|
private final boolean listResponse;
|
||||||
|
|
||||||
|
private final List<String> command;
|
||||||
|
|
||||||
|
private DockerCliCommand(Type type, Class<?> responseType, boolean listResponse, String... command) {
|
||||||
|
this.type = type;
|
||||||
|
this.responseType = responseType;
|
||||||
|
this.listResponse = listResponse;
|
||||||
|
this.command = List.of(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
Type getType() {
|
||||||
|
return this.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> getCommand() {
|
||||||
|
return this.command;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
R deserialize(String json) {
|
||||||
|
if (this.responseType == Void.class) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (R) ((!this.listResponse) ? DockerJson.deserialize(json, this.responseType)
|
||||||
|
: DockerJson.deserializeToList(json, this.responseType));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
DockerCliCommand<?> other = (DockerCliCommand<?>) obj;
|
||||||
|
boolean result = true;
|
||||||
|
result = result && this.type == other.type;
|
||||||
|
result = result && this.responseType == other.responseType;
|
||||||
|
result = result && this.listResponse == other.listResponse;
|
||||||
|
result = result && this.command.equals(other.command);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(this.type, this.responseType, this.listResponse, this.command);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "DockerCliCommand [type=%s, responseType=%s, listResponse=%s, command=%s]".formatted(this.type,
|
||||||
|
this.responseType, this.listResponse, this.command);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static String[] join(Collection<String> command, Collection<String> args) {
|
||||||
|
List<String> result = new ArrayList<>(command);
|
||||||
|
result.addAll(args);
|
||||||
|
return result.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@code docker context} command.
|
||||||
|
*/
|
||||||
|
static final class Context extends DockerCliCommand<List<DockerCliContextResponse>> {
|
||||||
|
|
||||||
|
Context() {
|
||||||
|
super(Type.DOCKER, DockerCliContextResponse.class, true, "context", "ls", "--format={{ json . }}");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@code docker inspect} command.
|
||||||
|
*/
|
||||||
|
static final class Inspect extends DockerCliCommand<List<DockerCliInspectResponse>> {
|
||||||
|
|
||||||
|
Inspect(Collection<String> ids) {
|
||||||
|
super(Type.DOCKER, DockerCliInspectResponse.class, true,
|
||||||
|
join(List.of("inspect", "--format={{ json . }}"), ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@code docker compose config} command.
|
||||||
|
*/
|
||||||
|
static final class ComposeConfig extends DockerCliCommand<DockerCliComposeConfigResponse> {
|
||||||
|
|
||||||
|
ComposeConfig() {
|
||||||
|
super(Type.DOCKER_COMPOSE, DockerCliComposeConfigResponse.class, false, "config", "--format=json");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@code docker compose ps} command.
|
||||||
|
*/
|
||||||
|
static final class ComposePs extends DockerCliCommand<List<DockerCliComposePsResponse>> {
|
||||||
|
|
||||||
|
ComposePs() {
|
||||||
|
super(Type.DOCKER_COMPOSE, DockerCliComposePsResponse.class, true, "ps", "--format=json");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@code docker compose up} command.
|
||||||
|
*/
|
||||||
|
static final class ComposeUp extends DockerCliCommand<Void> {
|
||||||
|
|
||||||
|
ComposeUp() {
|
||||||
|
super(Type.DOCKER_COMPOSE, Void.class, false, "up", "--no-color", "--quiet-pull", "--detach", "--wait");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@code docker compose down} command.
|
||||||
|
*/
|
||||||
|
static final class ComposeDown extends DockerCliCommand<Void> {
|
||||||
|
|
||||||
|
ComposeDown(Duration timeout) {
|
||||||
|
super(Type.DOCKER_COMPOSE, Void.class, false, "down", "--timeout", Long.toString(timeout.toSeconds()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@code docker compose start} command.
|
||||||
|
*/
|
||||||
|
static final class ComposeStart extends DockerCliCommand<Void> {
|
||||||
|
|
||||||
|
ComposeStart() {
|
||||||
|
super(Type.DOCKER_COMPOSE, Void.class, false, "start", "--no-color", "--quiet-pull", "--detach", "--wait");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@code docker compose stop} command.
|
||||||
|
*/
|
||||||
|
static final class ComposeStop extends DockerCliCommand<Void> {
|
||||||
|
|
||||||
|
ComposeStop(Duration timeout) {
|
||||||
|
super(Type.DOCKER_COMPOSE, Void.class, false, "stop", "--timeout", Long.toString(timeout.toSeconds()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command Types.
|
||||||
|
*/
|
||||||
|
enum Type {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A command executed using {@code docker}.
|
||||||
|
*/
|
||||||
|
DOCKER,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A command executed using {@code docker compose} or {@code docker-compose}.
|
||||||
|
*/
|
||||||
|
DOCKER_COMPOSE
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from {@link DockerCliCommand.ComposeConfig docker compose config}.
|
||||||
|
*
|
||||||
|
* @param name project name
|
||||||
|
* @param services services
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
record DockerCliComposeConfigResponse(String name, Map<String, DockerCliComposeConfigResponse.Service> services) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Docker compose service.
|
||||||
|
*
|
||||||
|
* @param image the image
|
||||||
|
*/
|
||||||
|
record Service(String image) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from {@link DockerCliCommand.ComposePs docker compose ps}.
|
||||||
|
*
|
||||||
|
* @param id the container ID
|
||||||
|
* @param name the name of the service
|
||||||
|
* @param image the image reference
|
||||||
|
* @param state the state of the container
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
record DockerCliComposePsResponse(String id, String name, String image, String state) {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from {@code docker compose version}.
|
||||||
|
*
|
||||||
|
* @param version docker compose version
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
record DockerCliComposeVersionResponse(String version) {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from {@link DockerCliCommand.Context docker context}.
|
||||||
|
*
|
||||||
|
* @param name the name of the context
|
||||||
|
* @param current if the context is the current one
|
||||||
|
* @param dockerEndpoint the endpoint of the docker daemon
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
record DockerCliContextResponse(String name, boolean current, String dockerEndpoint) {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from {@link DockerCliCommand.Inspect docker inspect}.
|
||||||
|
*
|
||||||
|
* @param id the container id
|
||||||
|
* @param config the config
|
||||||
|
* @param hostConfig the host config
|
||||||
|
* @param networkSettings the network settings
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
record DockerCliInspectResponse(String id, DockerCliInspectResponse.Config config,
|
||||||
|
DockerCliInspectResponse.NetworkSettings networkSettings, DockerCliInspectResponse.HostConfig hostConfig) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for the container that is portable between hosts.
|
||||||
|
*
|
||||||
|
* @param image the name (or reference) of the image
|
||||||
|
* @param labels user-defined key/value metadata
|
||||||
|
* @param exposedPorts the mapping of exposed ports
|
||||||
|
* @param env a list of environment variables in the form {@code VAR=value}
|
||||||
|
*/
|
||||||
|
record Config(String image, Map<String, String> labels, Map<String, ExposedPort> exposedPorts, List<String> env) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty object used with {@link Config#exposedPorts()}.
|
||||||
|
*/
|
||||||
|
record ExposedPort() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container's resources (cgroups config, ulimits, etc).
|
||||||
|
*
|
||||||
|
* @param networkMode the network mode to use for this container
|
||||||
|
*/
|
||||||
|
record HostConfig(String networkMode) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The network settings in the API.
|
||||||
|
*
|
||||||
|
* @param ports the mapping of container ports to host ports
|
||||||
|
*/
|
||||||
|
record NetworkSettings(Map<String, List<HostPort>> ports) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Port mapping details.
|
||||||
|
*
|
||||||
|
* @param hostIp the host IP
|
||||||
|
* @param hostPort the host port
|
||||||
|
*/
|
||||||
|
record HostPort(String hostIp, String hostPort) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a high-level API to work with Docker compose.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public interface DockerCompose {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeout duration used to request a forced shutdown.
|
||||||
|
*/
|
||||||
|
Duration FORCE_SHUTDOWN = Duration.ZERO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run {@code docker compose up} to startup services. Waits until all contains are
|
||||||
|
* started and healthy.
|
||||||
|
*/
|
||||||
|
void up();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run {@code docker compose down} to shutdown any running services.
|
||||||
|
* @param timeout the amount of time to wait or {@link #FORCE_SHUTDOWN} to shutdown
|
||||||
|
* without waiting.
|
||||||
|
*/
|
||||||
|
void down(Duration timeout);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run {@code docker compose start} to startup services. Waits until all contains are
|
||||||
|
* started and healthy.
|
||||||
|
*/
|
||||||
|
void start();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run {@code docker compose stop} to shutdown any running services.
|
||||||
|
* @param timeout the amount of time to wait or {@link #FORCE_SHUTDOWN} to shutdown
|
||||||
|
* without waiting.
|
||||||
|
*/
|
||||||
|
void stop(Duration timeout);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return if services have been defined in the {@link DockerComposeFile} for the
|
||||||
|
* active profiles.
|
||||||
|
* @return {@code true} if services have been defined
|
||||||
|
* @see #hasDefinedServices()
|
||||||
|
*/
|
||||||
|
boolean hasDefinedServices();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return if services defined in the {@link DockerComposeFile} for the active profile
|
||||||
|
* are running.
|
||||||
|
* @return {@code true} if services are running
|
||||||
|
* @see #hasDefinedServices()
|
||||||
|
* @see #getRunningServices()
|
||||||
|
*/
|
||||||
|
boolean hasRunningServices();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the running services for the active profile, or an empty list if no services
|
||||||
|
* are running.
|
||||||
|
* @return the list of running services
|
||||||
|
*/
|
||||||
|
List<RunningService> getRunningServices();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method used to create a {@link DockerCompose} instance.
|
||||||
|
* @param file the docker compose file
|
||||||
|
* @param hostname the hostname used for services or {@code null} if the hostname
|
||||||
|
* should be deduced
|
||||||
|
* @param activeProfiles a set of the profiles that should be activated
|
||||||
|
* @return a {@link DockerCompose} instance
|
||||||
|
*/
|
||||||
|
static DockerCompose get(DockerComposeFile file, String hostname, Set<String> activeProfiles) {
|
||||||
|
DockerCli cli = new DockerCli(null, file, activeProfiles);
|
||||||
|
return new DefaultDockerCompose(cli, hostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reference to a docker compose file (usually named {@code compose.yaml}).
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
* @see #of(File)
|
||||||
|
* @see #find(File)
|
||||||
|
*/
|
||||||
|
public final class DockerComposeFile {
|
||||||
|
|
||||||
|
private static final List<String> SEARCH_ORDER = List.of("compose.yaml", "compose.yml", "docker-compose.yaml",
|
||||||
|
"docker-compose.yml");
|
||||||
|
|
||||||
|
private final File file;
|
||||||
|
|
||||||
|
private DockerComposeFile(File file) {
|
||||||
|
try {
|
||||||
|
this.file = file.getCanonicalFile();
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new UncheckedIOException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
DockerComposeFile other = (DockerComposeFile) obj;
|
||||||
|
return this.file.equals(other.file);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return this.file.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.file.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the docker compose file by searching in the given working directory. Files are
|
||||||
|
* considered in the same order that {@code docker compose} uses, namely:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code compose.yaml}</li>
|
||||||
|
* <li>{@code compose.yml}</li>
|
||||||
|
* <li>{@code docker-compose.yaml}</li>
|
||||||
|
* <li>{@code docker-compose.yml}</li>
|
||||||
|
* </ul>
|
||||||
|
* @param workingDirectory the working directory to search or {@code null} to use the
|
||||||
|
* current directory
|
||||||
|
* @return the located file or {@code null} if no docker compose file can be found
|
||||||
|
*/
|
||||||
|
public static DockerComposeFile find(File workingDirectory) {
|
||||||
|
File base = (workingDirectory != null) ? workingDirectory : new File(".");
|
||||||
|
if (!base.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Assert.isTrue(base.isDirectory(), () -> "'%s' is not a directory".formatted(base));
|
||||||
|
Path basePath = base.toPath();
|
||||||
|
for (String candidate : SEARCH_ORDER) {
|
||||||
|
Path resolved = basePath.resolve(candidate);
|
||||||
|
if (Files.exists(resolved)) {
|
||||||
|
return of(resolved.toAbsolutePath().toFile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link DockerComposeFile} for the given {@link File}.
|
||||||
|
* @param file the source file
|
||||||
|
* @return the docker compose file
|
||||||
|
*/
|
||||||
|
public static DockerComposeFile of(File file) {
|
||||||
|
Assert.notNull(file, "File must not be null");
|
||||||
|
Assert.isTrue(file.exists(), () -> "'%s' does not exist".formatted(file));
|
||||||
|
Assert.isTrue(file.isFile(), () -> "'%s' is not a file".formatted(file));
|
||||||
|
return new DockerComposeFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import org.springframework.boot.origin.Origin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An origin which points to a service defined in docker compose.
|
||||||
|
*
|
||||||
|
* @param composeFile docker compose file
|
||||||
|
* @param serviceName name of the docker compose service
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public record DockerComposeOrigin(DockerComposeFile composeFile, String serviceName) implements Origin {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Docker compose service '%s' defined in '%s'".formatted(this.serviceName,
|
||||||
|
(this.composeFile != null) ? this.composeFile : "default compose file");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses and provides access to docker {@code env} data.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DockerEnv {
|
||||||
|
|
||||||
|
private final Map<String, String> map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link DockerEnv} instance.
|
||||||
|
* @param env a list of env entries in the form {@code name=value} or {@code name}.
|
||||||
|
*/
|
||||||
|
DockerEnv(List<String> env) {
|
||||||
|
this.map = parse(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> parse(List<String> env) {
|
||||||
|
if (CollectionUtils.isEmpty(env)) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
Map<String, String> result = new LinkedHashMap<>();
|
||||||
|
env.stream().map(this::parseEntry).forEach((entry) -> result.put(entry.key(), entry.value()));
|
||||||
|
return Collections.unmodifiableMap(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Entry parseEntry(String entry) {
|
||||||
|
int index = entry.indexOf('=');
|
||||||
|
if (index != -1) {
|
||||||
|
String key = entry.substring(0, index);
|
||||||
|
String value = entry.substring(index + 1);
|
||||||
|
return new Entry(key, value);
|
||||||
|
}
|
||||||
|
return new Entry(entry, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the env as a {@link Map}.
|
||||||
|
* @return the env as a map
|
||||||
|
*/
|
||||||
|
Map<String, String> asMap() {
|
||||||
|
return this.map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Entry(String key, String value) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for docker exceptions.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public abstract class DockerException extends RuntimeException {
|
||||||
|
|
||||||
|
public DockerException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DockerException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A docker host as defined by the user or deduced.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
final class DockerHost {
|
||||||
|
|
||||||
|
private static final String LOCALHOST = "127.0.0.1";
|
||||||
|
|
||||||
|
private String host;
|
||||||
|
|
||||||
|
private DockerHost(String host) {
|
||||||
|
this.host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
DockerHost other = (DockerHost) obj;
|
||||||
|
return this.host.equals(other.host);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return this.host.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or deduce a new {@link DockerHost} instance.
|
||||||
|
* @param host the host to use or {@code null} to deduce
|
||||||
|
* @param contextsSupplier a supplier to provide a list of
|
||||||
|
* {@link DockerCliContextResponse}
|
||||||
|
* @return a new docker host instance
|
||||||
|
*/
|
||||||
|
static DockerHost get(String host, Supplier<List<DockerCliContextResponse>> contextsSupplier) {
|
||||||
|
return get(host, System::getenv, contextsSupplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or deduce a new {@link DockerHost} instance.
|
||||||
|
* @param host the host to use or {@code null} to deduce
|
||||||
|
* @param systemEnv access to the system environment
|
||||||
|
* @param contextsSupplier a supplier to provide a list of
|
||||||
|
* {@link DockerCliContextResponse}
|
||||||
|
* @return a new docker host instance
|
||||||
|
*/
|
||||||
|
static DockerHost get(String host, Function<String, String> systemEnv,
|
||||||
|
Supplier<List<DockerCliContextResponse>> contextsSupplier) {
|
||||||
|
host = (StringUtils.hasText(host)) ? host : fromServicesHostEnv(systemEnv);
|
||||||
|
host = (StringUtils.hasText(host)) ? host : fromDockerHostEnv(systemEnv);
|
||||||
|
host = (StringUtils.hasText(host)) ? host : fromCurrentContext(contextsSupplier);
|
||||||
|
host = (StringUtils.hasText(host)) ? host : LOCALHOST;
|
||||||
|
return new DockerHost(host);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fromServicesHostEnv(Function<String, String> systemEnv) {
|
||||||
|
return systemEnv.apply("SERVICES_HOST");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fromDockerHostEnv(Function<String, String> systemEnv) {
|
||||||
|
return fromEndpoint(systemEnv.apply("DOCKER_HOST"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fromCurrentContext(Supplier<List<DockerCliContextResponse>> contextsSupplier) {
|
||||||
|
DockerCliContextResponse current = getCurrentContext(contextsSupplier.get());
|
||||||
|
return (current != null) ? fromEndpoint(current.dockerEndpoint()) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DockerCliContextResponse getCurrentContext(List<DockerCliContextResponse> candidates) {
|
||||||
|
return candidates.stream().filter(DockerCliContextResponse::current).findFirst().orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fromEndpoint(String endpoint) {
|
||||||
|
return (StringUtils.hasLength(endpoint)) ? fromUri(URI.create(endpoint)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fromUri(URI uri) {
|
||||||
|
try {
|
||||||
|
return switch (uri.getScheme()) {
|
||||||
|
case "http", "https", "tcp" -> uri.getHost();
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.JavaType;
|
||||||
|
import com.fasterxml.jackson.databind.MapperFeature;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||||
|
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support class used to handle JSON returned from the {@link DockerCli}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
final class DockerJson {
|
||||||
|
|
||||||
|
private static final ObjectMapper objectMapper = JsonMapper.builder()
|
||||||
|
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
|
||||||
|
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
|
||||||
|
.addModule(new ParameterNamesModule())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private DockerJson() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialize JSON to a list. Handles JSON arrays and multiple JSON objects in
|
||||||
|
* separate lines.
|
||||||
|
* @param <T> the item type
|
||||||
|
* @param json the source JSON
|
||||||
|
* @param itemType the item type
|
||||||
|
* @return a list of items
|
||||||
|
*/
|
||||||
|
static <T> List<T> deserializeToList(String json, Class<T> itemType) {
|
||||||
|
if (json.startsWith("[")) {
|
||||||
|
JavaType javaType = objectMapper.getTypeFactory().constructCollectionType(List.class, itemType);
|
||||||
|
return deserialize(json, javaType);
|
||||||
|
}
|
||||||
|
return json.trim().lines().map((line) -> deserialize(line, itemType)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialize JSON to an object instance.
|
||||||
|
* @param <T> the result type
|
||||||
|
* @param json the source JSON
|
||||||
|
* @param type the result type
|
||||||
|
* @return the deserialized result
|
||||||
|
*/
|
||||||
|
static <T> T deserialize(String json, Class<T> type) {
|
||||||
|
return deserialize(json, objectMapper.getTypeFactory().constructType(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T deserialize(String json, JavaType type) {
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(json.trim(), type);
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new DockerOutputParseException(json, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DockerException} thrown if the docker daemon is not running.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public class DockerNotRunningException extends DockerException {
|
||||||
|
|
||||||
|
private final String errorOutput;
|
||||||
|
|
||||||
|
DockerNotRunningException(String errorOutput, Throwable cause) {
|
||||||
|
super("Docker is not running", cause);
|
||||||
|
this.errorOutput = errorOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the error output returned from docker.
|
||||||
|
* @return the error output
|
||||||
|
*/
|
||||||
|
public String getErrorOutput() {
|
||||||
|
return this.errorOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DockerException} thrown if the docker JSON cannot be parsed.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public class DockerOutputParseException extends DockerException {
|
||||||
|
|
||||||
|
DockerOutputParseException(String json, Throwable cause) {
|
||||||
|
super("Failed to parse docker JSON:\n\n" + json, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DockerException} thrown if the docker process cannot be started. Usually
|
||||||
|
* indicates that docker is not installed.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public class DockerProcessStartException extends DockerException {
|
||||||
|
|
||||||
|
DockerProcessStartException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A docker image reference of form
|
||||||
|
* {@code [<registry>/][<project>/]<image>[:<tag>|@<digest>]}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
* @see <a href="https://docs.docker.com/compose/compose-file/#image">docker
|
||||||
|
* documentation</a>
|
||||||
|
*/
|
||||||
|
public final class ImageReference {
|
||||||
|
|
||||||
|
private final String reference;
|
||||||
|
|
||||||
|
private final String imageName;
|
||||||
|
|
||||||
|
ImageReference(String reference) {
|
||||||
|
this.reference = reference;
|
||||||
|
int lastSlashIndex = reference.lastIndexOf('/');
|
||||||
|
String imageTagDigest = (lastSlashIndex != -1) ? reference.substring(lastSlashIndex + 1) : reference;
|
||||||
|
int digestIndex = imageTagDigest.indexOf('@');
|
||||||
|
String imageTag = (digestIndex != -1) ? imageTagDigest.substring(0, digestIndex) : imageTagDigest;
|
||||||
|
int colon = imageTag.indexOf(':');
|
||||||
|
this.imageName = (colon != -1) ? imageTag.substring(0, colon) : imageTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ImageReference other = (ImageReference) obj;
|
||||||
|
return this.reference.equals(other.reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return this.reference.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the referenced image, excluding the registry or project. For example, a
|
||||||
|
* reference of {@code my_private.registry:5000/redis:5} would return {@code redis}.
|
||||||
|
* @return the referenced image
|
||||||
|
*/
|
||||||
|
public String getImageName() {
|
||||||
|
return this.imageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an image reference from the given String value.
|
||||||
|
* @param value the string used to create the reference
|
||||||
|
* @return an {@link ImageReference} instance
|
||||||
|
*/
|
||||||
|
public static ImageReference of(String value) {
|
||||||
|
return (value != null) ? new ImageReference(value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown by {@link ProcessRunner} when the process exits with a non-zero code.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class ProcessExitException extends RuntimeException {
|
||||||
|
|
||||||
|
private final int exitCode;
|
||||||
|
|
||||||
|
private final String[] command;
|
||||||
|
|
||||||
|
private final String stdOut;
|
||||||
|
|
||||||
|
private final String stdErr;
|
||||||
|
|
||||||
|
ProcessExitException(int exitCode, String[] command, String stdOut, String stdErr) {
|
||||||
|
this(exitCode, command, stdOut, stdErr, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessExitException(int exitCode, String[] command, String stdOut, String stdErr, Throwable cause) {
|
||||||
|
super(buildMessage(exitCode, command, stdOut, stdErr), cause);
|
||||||
|
this.exitCode = exitCode;
|
||||||
|
this.command = command;
|
||||||
|
this.stdOut = stdOut;
|
||||||
|
this.stdErr = stdErr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildMessage(int exitCode, String[] command, String stdOut, String strErr) {
|
||||||
|
return "'%s' failed with exit code %d.\n\nStdout:\n%s\n\nStderr:\n%s".formatted(String.join(" ", command),
|
||||||
|
exitCode, stdOut, strErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getExitCode() {
|
||||||
|
return this.exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] getCommand() {
|
||||||
|
return this.command;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getStdOut() {
|
||||||
|
return this.stdOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getStdErr() {
|
||||||
|
return this.stdErr;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
|
import org.springframework.core.log.LogMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a process and captures the result.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class ProcessRunner {
|
||||||
|
|
||||||
|
private static final String USR_LOCAL_BIN = "/usr/local/bin";
|
||||||
|
|
||||||
|
private static final boolean MAC_OS = System.getProperty("os.name").toLowerCase().contains("mac");
|
||||||
|
|
||||||
|
private static final Log logger = LogFactory.getLog(ProcessRunner.class);
|
||||||
|
|
||||||
|
private final File workingDirectory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link ProcessRunner} instance.
|
||||||
|
*/
|
||||||
|
ProcessRunner() {
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link ProcessRunner} instance.
|
||||||
|
* @param workingDirectory the working directory for the process
|
||||||
|
*/
|
||||||
|
ProcessRunner(File workingDirectory) {
|
||||||
|
this.workingDirectory = workingDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the given {@code command}. If the process exits with an error code other than
|
||||||
|
* zero, an {@link ProcessExitException} will be thrown.
|
||||||
|
* @param command the command to run
|
||||||
|
* @return the output of the command
|
||||||
|
* @throws ProcessExitException if execution failed
|
||||||
|
*/
|
||||||
|
String run(String... command) {
|
||||||
|
logger.trace(LogMessage.of(() -> "Running '%s'".formatted(String.join(" ", command))));
|
||||||
|
Process process = startProcess(command);
|
||||||
|
ReaderThread stdOutReader = new ReaderThread(process.getInputStream(), "stdout");
|
||||||
|
ReaderThread stdErrReader = new ReaderThread(process.getErrorStream(), "stderr");
|
||||||
|
logger.trace("Waiting for process exit");
|
||||||
|
int exitCode = waitForProcess(process);
|
||||||
|
logger.trace(LogMessage.format("Process exited with exit code %d", exitCode));
|
||||||
|
String stdOut = stdOutReader.toString();
|
||||||
|
String stdErr = stdErrReader.toString();
|
||||||
|
if (exitCode != 0) {
|
||||||
|
throw new ProcessExitException(exitCode, command, stdOut, stdErr);
|
||||||
|
}
|
||||||
|
return stdOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Process startProcess(String[] command) {
|
||||||
|
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||||
|
processBuilder.directory(this.workingDirectory);
|
||||||
|
try {
|
||||||
|
return processBuilder.start();
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
String path = processBuilder.environment().get("PATH");
|
||||||
|
if (MAC_OS && path != null && !path.contains(USR_LOCAL_BIN)
|
||||||
|
&& !command[0].startsWith(USR_LOCAL_BIN + "/")) {
|
||||||
|
String[] localCommand = command.clone();
|
||||||
|
localCommand[0] = USR_LOCAL_BIN + "/" + localCommand[0];
|
||||||
|
return startProcess(localCommand);
|
||||||
|
}
|
||||||
|
throw new ProcessStartException(command, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int waitForProcess(Process process) {
|
||||||
|
try {
|
||||||
|
return process.waitFor();
|
||||||
|
}
|
||||||
|
catch (InterruptedException ex) {
|
||||||
|
throw new IllegalStateException("Interrupted waiting for %s".formatted(process));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thread used to read stream input from the process.
|
||||||
|
*/
|
||||||
|
private static class ReaderThread extends Thread {
|
||||||
|
|
||||||
|
private final InputStream source;
|
||||||
|
|
||||||
|
private final ByteArrayOutputStream content = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
private final CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
ReaderThread(InputStream source, String name) {
|
||||||
|
this.source = source;
|
||||||
|
setName("OutputReader-" + name);
|
||||||
|
setDaemon(true);
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
this.source.transferTo(this.content);
|
||||||
|
this.latch.countDown();
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new UncheckedIOException("Failed to read process stream", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
try {
|
||||||
|
this.latch.await();
|
||||||
|
return new String(this.content.toByteArray(), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
catch (InterruptedException ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown by {@link ProcessRunner} when a processes will not start.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class ProcessStartException extends RuntimeException {
|
||||||
|
|
||||||
|
ProcessStartException(String[] command, IOException ex) {
|
||||||
|
super("Unable to start command %s".formatted(String.join(" ", command)), ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides details of a running docker compose service.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public interface RunningService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the name of the service.
|
||||||
|
* @return the service name
|
||||||
|
*/
|
||||||
|
String name();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the image being used by the service.
|
||||||
|
* @return the service image
|
||||||
|
*/
|
||||||
|
ImageReference image();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the host that can be used to connect to the service.
|
||||||
|
* @return the service host
|
||||||
|
*/
|
||||||
|
String host();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the ports that can be used to connect to the service.
|
||||||
|
* @return the service ports
|
||||||
|
*/
|
||||||
|
ConnectionPorts ports();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the environment defined for the service.
|
||||||
|
* @return the service env
|
||||||
|
*/
|
||||||
|
Map<String, String> env();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the labels attached to the service.
|
||||||
|
* @return the service labels
|
||||||
|
*/
|
||||||
|
Map<String, String> labels();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core interfaces and classes for working with docker compose.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
@ -0,0 +1,150 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.lifecycle;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplicationShutdownHandlers;
|
||||||
|
import org.springframework.boot.context.properties.bind.Binder;
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCompose;
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerComposeFile;
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Shutdown;
|
||||||
|
import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Startup;
|
||||||
|
import org.springframework.boot.docker.compose.readiness.ServiceReadinessChecks;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationListener;
|
||||||
|
import org.springframework.context.event.SimpleApplicationEventMulticaster;
|
||||||
|
import org.springframework.core.log.LogMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the lifecycle for docker compose services.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @see DockerComposeListener
|
||||||
|
*/
|
||||||
|
class DockerComposeLifecycleManager {
|
||||||
|
|
||||||
|
private static final Log logger = LogFactory.getLog(DockerComposeLifecycleManager.class);
|
||||||
|
|
||||||
|
private static final Object IGNORE_LABEL = "org.springframework.boot.ignore";
|
||||||
|
|
||||||
|
private final File workingDirectory;
|
||||||
|
|
||||||
|
private final ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
private final ClassLoader classLoader;
|
||||||
|
|
||||||
|
private final SpringApplicationShutdownHandlers shutdownHandlers;
|
||||||
|
|
||||||
|
private final DockerComposeProperties properties;
|
||||||
|
|
||||||
|
private final Set<ApplicationListener<?>> eventListeners;
|
||||||
|
|
||||||
|
private final DockerComposeSkipCheck skipCheck;
|
||||||
|
|
||||||
|
private final ServiceReadinessChecks serviceReadinessChecks;
|
||||||
|
|
||||||
|
DockerComposeLifecycleManager(ApplicationContext applicationContext, Binder binder,
|
||||||
|
SpringApplicationShutdownHandlers shutdownHandlers, DockerComposeProperties properties,
|
||||||
|
Set<ApplicationListener<?>> eventListeners) {
|
||||||
|
this(null, applicationContext, binder, shutdownHandlers, properties, eventListeners,
|
||||||
|
new DockerComposeSkipCheck(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
DockerComposeLifecycleManager(File workingDirectory, ApplicationContext applicationContext, Binder binder,
|
||||||
|
SpringApplicationShutdownHandlers shutdownHandlers, DockerComposeProperties properties,
|
||||||
|
Set<ApplicationListener<?>> eventListeners, DockerComposeSkipCheck skipCheck,
|
||||||
|
ServiceReadinessChecks serviceReadinessChecks) {
|
||||||
|
this.workingDirectory = workingDirectory;
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
this.classLoader = applicationContext.getClassLoader();
|
||||||
|
this.shutdownHandlers = shutdownHandlers;
|
||||||
|
this.properties = properties;
|
||||||
|
this.eventListeners = eventListeners;
|
||||||
|
this.skipCheck = skipCheck;
|
||||||
|
this.serviceReadinessChecks = (serviceReadinessChecks != null) ? serviceReadinessChecks
|
||||||
|
: new ServiceReadinessChecks(this.classLoader, applicationContext.getEnvironment(), binder);
|
||||||
|
}
|
||||||
|
|
||||||
|
void startup() {
|
||||||
|
if (!this.properties.isEnabled()) {
|
||||||
|
logger.trace("Docker compose support not enabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.skipCheck.shouldSkip(this.classLoader, logger, this.properties.getSkip())) {
|
||||||
|
logger.trace("Docker compose support skipped");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DockerComposeFile composeFile = getComposeFile();
|
||||||
|
Set<String> activeProfiles = this.properties.getProfiles().getActive();
|
||||||
|
DockerCompose dockerCompose = getDockerCompose(composeFile, activeProfiles);
|
||||||
|
if (!dockerCompose.hasDefinedServices()) {
|
||||||
|
logger.warn(LogMessage.format("No services defined in docker compose file '%s' with active profiles %s",
|
||||||
|
composeFile, activeProfiles));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LifecycleManagement lifecycleManagement = this.properties.getLifecycleManagement();
|
||||||
|
Startup startup = this.properties.getStartup();
|
||||||
|
Shutdown shutdown = this.properties.getShutdown();
|
||||||
|
if (lifecycleManagement.shouldStartup() && !dockerCompose.hasRunningServices()) {
|
||||||
|
startup.getCommand().applyTo(dockerCompose);
|
||||||
|
if (lifecycleManagement.shouldShutdown()) {
|
||||||
|
this.shutdownHandlers.add(() -> shutdown.getCommand().applyTo(dockerCompose, shutdown.getTimeout()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<RunningService> runningServices = new ArrayList<>(dockerCompose.getRunningServices());
|
||||||
|
runningServices.removeIf(this::isIgnored);
|
||||||
|
this.serviceReadinessChecks.waitUntilReady(runningServices);
|
||||||
|
publishEvent(new DockerComposeServicesReadyEvent(this.applicationContext, runningServices));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected DockerComposeFile getComposeFile() {
|
||||||
|
DockerComposeFile composeFile = (this.properties.getFile() != null)
|
||||||
|
? DockerComposeFile.of(this.properties.getFile()) : DockerComposeFile.find(this.workingDirectory);
|
||||||
|
logger.info(LogMessage.format("Found docker compose file '%s'", composeFile));
|
||||||
|
return composeFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected DockerCompose getDockerCompose(DockerComposeFile composeFile, Set<String> activeProfiles) {
|
||||||
|
return DockerCompose.get(composeFile, this.properties.getHost(), activeProfiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isIgnored(RunningService service) {
|
||||||
|
return service.labels().containsKey(IGNORE_LABEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish a {@link DockerComposeServicesReadyEvent} directly to the event listeners
|
||||||
|
* since we cannot call {@link ApplicationContext#publishEvent} this early.
|
||||||
|
* @param event the event to publish
|
||||||
|
*/
|
||||||
|
private void publishEvent(DockerComposeServicesReadyEvent event) {
|
||||||
|
SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
|
||||||
|
this.eventListeners.forEach(multicaster::addApplicationListener);
|
||||||
|
multicaster.multicastEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.lifecycle;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.SpringApplicationShutdownHandlers;
|
||||||
|
import org.springframework.boot.context.event.ApplicationPreparedEvent;
|
||||||
|
import org.springframework.boot.context.properties.bind.Binder;
|
||||||
|
import org.springframework.context.ApplicationListener;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ApplicationListener} used to setup a {@link DockerComposeLifecycleManager}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DockerComposeListener implements ApplicationListener<ApplicationPreparedEvent> {
|
||||||
|
|
||||||
|
private final SpringApplicationShutdownHandlers shutdownHandlers;
|
||||||
|
|
||||||
|
DockerComposeListener() {
|
||||||
|
this(SpringApplication.getShutdownHandlers());
|
||||||
|
}
|
||||||
|
|
||||||
|
DockerComposeListener(SpringApplicationShutdownHandlers shutdownHandlers) {
|
||||||
|
this.shutdownHandlers = SpringApplication.getShutdownHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplicationEvent(ApplicationPreparedEvent event) {
|
||||||
|
ConfigurableApplicationContext applicationContext = event.getApplicationContext();
|
||||||
|
Binder binder = Binder.get(applicationContext.getEnvironment());
|
||||||
|
DockerComposeProperties properties = DockerComposeProperties.get(binder);
|
||||||
|
Set<ApplicationListener<?>> eventListeners = event.getSpringApplication().getListeners();
|
||||||
|
createDockerComposeLifecycleManager(applicationContext, binder, properties, eventListeners).startup();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected DockerComposeLifecycleManager createDockerComposeLifecycleManager(
|
||||||
|
ConfigurableApplicationContext applicationContext, Binder binder, DockerComposeProperties properties,
|
||||||
|
Set<ApplicationListener<?>> eventListeners) {
|
||||||
|
return new DockerComposeLifecycleManager(applicationContext, binder, this.shutdownHandlers, properties,
|
||||||
|
eventListeners);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,222 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.lifecycle;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.boot.context.properties.bind.Binder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration properties for the 'docker compose'.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
@ConfigurationProperties(DockerComposeProperties.NAME)
|
||||||
|
public class DockerComposeProperties {
|
||||||
|
|
||||||
|
static final String NAME = "spring.docker.compose";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether docker compose support is enabled.
|
||||||
|
*/
|
||||||
|
private boolean enabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to a specific docker compose configuration file.
|
||||||
|
*/
|
||||||
|
private File file;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Docker compose lifecycle management.
|
||||||
|
*/
|
||||||
|
private LifecycleManagement lifecycleManagement = LifecycleManagement.START_AND_STOP;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hostname or IP of the machine where the docker containers are started.
|
||||||
|
*/
|
||||||
|
private String host;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start configuration.
|
||||||
|
*/
|
||||||
|
private final Startup startup = new Startup();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop configuration.
|
||||||
|
*/
|
||||||
|
private final Shutdown shutdown = new Shutdown();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profiles configuration.
|
||||||
|
*/
|
||||||
|
private final Profiles profiles = new Profiles();
|
||||||
|
|
||||||
|
private final Skip skip = new Skip();
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return this.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getFile() {
|
||||||
|
return this.file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFile(File file) {
|
||||||
|
this.file = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LifecycleManagement getLifecycleManagement() {
|
||||||
|
return this.lifecycleManagement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLifecycleManagement(LifecycleManagement lifecycleManagement) {
|
||||||
|
this.lifecycleManagement = lifecycleManagement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHost() {
|
||||||
|
return this.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHost(String host) {
|
||||||
|
this.host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Startup getStartup() {
|
||||||
|
return this.startup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Shutdown getShutdown() {
|
||||||
|
return this.shutdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Profiles getProfiles() {
|
||||||
|
return this.profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Skip getSkip() {
|
||||||
|
return this.skip;
|
||||||
|
}
|
||||||
|
|
||||||
|
static DockerComposeProperties get(Binder binder) {
|
||||||
|
return binder.bind(NAME, DockerComposeProperties.class).orElseGet(DockerComposeProperties::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startup properties.
|
||||||
|
*/
|
||||||
|
public static class Startup {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command used to start docker compose.
|
||||||
|
*/
|
||||||
|
private StartupCommand command = StartupCommand.UP;
|
||||||
|
|
||||||
|
public StartupCommand getCommand() {
|
||||||
|
return this.command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCommand(StartupCommand command) {
|
||||||
|
this.command = command;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shutdown properties.
|
||||||
|
*/
|
||||||
|
public static class Shutdown {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command used to stop docker compose.
|
||||||
|
*/
|
||||||
|
private ShutdownCommand command = ShutdownCommand.DOWN;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeout for stopping docker compose. Use '0' for forced stop.
|
||||||
|
*/
|
||||||
|
private Duration timeout = Duration.ofSeconds(10);
|
||||||
|
|
||||||
|
public ShutdownCommand getCommand() {
|
||||||
|
return this.command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCommand(ShutdownCommand command) {
|
||||||
|
this.command = command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Duration getTimeout() {
|
||||||
|
return this.timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimeout(Duration timeout) {
|
||||||
|
this.timeout = timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profiles properties.
|
||||||
|
*/
|
||||||
|
public static class Profiles {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Docker compose profiles that should be active.
|
||||||
|
*/
|
||||||
|
private Set<String> active = new LinkedHashSet<>();
|
||||||
|
|
||||||
|
public Set<String> getActive() {
|
||||||
|
return this.active;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActive(Set<String> active) {
|
||||||
|
this.active = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skip options.
|
||||||
|
*/
|
||||||
|
public static class Skip {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to skip in tests.
|
||||||
|
*/
|
||||||
|
private boolean inTests = true;
|
||||||
|
|
||||||
|
public boolean isInTests() {
|
||||||
|
return this.inTests;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInTests(boolean inTests) {
|
||||||
|
this.inTests = inTests;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.lifecycle;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.event.ApplicationPreparedEvent;
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ApplicationEvent} published when docker compose {@link RunningService} instance
|
||||||
|
* are available. This even is published from the {@link ApplicationPreparedEvent} that
|
||||||
|
* performs the docker compose startup.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public class DockerComposeServicesReadyEvent extends ApplicationEvent {
|
||||||
|
|
||||||
|
private final List<RunningService> runningServices;
|
||||||
|
|
||||||
|
DockerComposeServicesReadyEvent(ApplicationContext source, List<RunningService> runningServices) {
|
||||||
|
super(source);
|
||||||
|
this.runningServices = runningServices;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ApplicationContext getSource() {
|
||||||
|
return (ApplicationContext) super.getSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the relevant docker compose services that are running.
|
||||||
|
* @return the running services
|
||||||
|
*/
|
||||||
|
public List<RunningService> getRunningServices() {
|
||||||
|
return this.runningServices;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.lifecycle;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplicationAotProcessor;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if docker compose support should be skipped.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DockerComposeSkipCheck {
|
||||||
|
|
||||||
|
private static final Set<String> REQUIRED_CLASSES = Set.of("org.junit.jupiter.api.Test", "org.junit.Test");
|
||||||
|
|
||||||
|
private static final Set<String> SKIPPED_STACK_ELEMENTS;
|
||||||
|
|
||||||
|
static {
|
||||||
|
Set<String> skipped = new LinkedHashSet<>();
|
||||||
|
skipped.add("org.junit.runners.");
|
||||||
|
skipped.add("org.junit.platform.");
|
||||||
|
skipped.add("org.springframework.boot.test.");
|
||||||
|
skipped.add(SpringApplicationAotProcessor.class.getName());
|
||||||
|
skipped.add("cucumber.runtime.");
|
||||||
|
SKIPPED_STACK_ELEMENTS = Collections.unmodifiableSet(skipped);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean shouldSkip(ClassLoader classLoader, Log logger, DockerComposeProperties.Skip properties) {
|
||||||
|
if (properties.isInTests() && hasAtLeastOneRequiredClass(classLoader)) {
|
||||||
|
Thread thread = Thread.currentThread();
|
||||||
|
for (StackTraceElement element : thread.getStackTrace()) {
|
||||||
|
if (isSkippedStackElement(element)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasAtLeastOneRequiredClass(ClassLoader classLoader) {
|
||||||
|
for (String requiredClass : REQUIRED_CLASSES) {
|
||||||
|
if (ClassUtils.isPresent(requiredClass, classLoader)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSkippedStackElement(StackTraceElement element) {
|
||||||
|
for (String skipped : SKIPPED_STACK_ELEMENTS) {
|
||||||
|
if (element.getClassName().startsWith(skipped)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.lifecycle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Docker compose lifecycle management.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public enum LifecycleManagement {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't start or stop docker compose.
|
||||||
|
*/
|
||||||
|
NONE(false, false),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only start docker compose if it's not running.
|
||||||
|
*/
|
||||||
|
START_ONLY(true, false),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start and stop docker compose if it's not running.
|
||||||
|
*/
|
||||||
|
START_AND_STOP(true, true);
|
||||||
|
|
||||||
|
private final boolean startup;
|
||||||
|
|
||||||
|
private final boolean shutdown;
|
||||||
|
|
||||||
|
LifecycleManagement(boolean startup, boolean shutdown) {
|
||||||
|
this.startup = startup;
|
||||||
|
this.shutdown = shutdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether docker compose should be started.
|
||||||
|
* @return whether docker compose should be started.
|
||||||
|
*/
|
||||||
|
boolean shouldStartup() {
|
||||||
|
return this.startup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether docker compose should be stopped.
|
||||||
|
* @return whether docker compose should be stopped
|
||||||
|
*/
|
||||||
|
boolean shouldShutdown() {
|
||||||
|
return this.shutdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.lifecycle;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCompose;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command used to shutdown docker compose.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public enum ShutdownCommand {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shutdown using {@code docker compose down}.
|
||||||
|
*/
|
||||||
|
DOWN(DockerCompose::down),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shutdown using {@code docker compose stop}.
|
||||||
|
*/
|
||||||
|
STOP(DockerCompose::stop);
|
||||||
|
|
||||||
|
private final BiConsumer<DockerCompose, Duration> action;
|
||||||
|
|
||||||
|
ShutdownCommand(BiConsumer<DockerCompose, Duration> action) {
|
||||||
|
this.action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyTo(DockerCompose dockerCompose, Duration timeout) {
|
||||||
|
this.action.accept(dockerCompose, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.lifecycle;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCompose;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command used to startup docker compose.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public enum StartupCommand {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startup using {@code docker compose up}.
|
||||||
|
*/
|
||||||
|
UP(DockerCompose::up),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startup using {@code docker compose start}.
|
||||||
|
*/
|
||||||
|
START(DockerCompose::start);
|
||||||
|
|
||||||
|
private final Consumer<DockerCompose> action;
|
||||||
|
|
||||||
|
StartupCommand(Consumer<DockerCompose> action) {
|
||||||
|
this.action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyTo(DockerCompose dockerCompose) {
|
||||||
|
this.action.accept(dockerCompose);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle management for docker compose with the context of a Spring application.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.docker.compose.lifecycle;
|
@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.readiness;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.boot.context.properties.bind.Binder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Readiness configuration properties.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
@ConfigurationProperties(ReadinessProperties.NAME)
|
||||||
|
public class ReadinessProperties {
|
||||||
|
|
||||||
|
static final String NAME = "spring.docker.compose.readiness";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeout of the readiness checks.
|
||||||
|
*/
|
||||||
|
private Duration timeout = Duration.ofMinutes(2);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TCP properties.
|
||||||
|
*/
|
||||||
|
private final Tcp tcp = new Tcp();
|
||||||
|
|
||||||
|
public Duration getTimeout() {
|
||||||
|
return this.timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimeout(Duration timeout) {
|
||||||
|
this.timeout = timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Tcp getTcp() {
|
||||||
|
return this.tcp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the properties using the given binder.
|
||||||
|
* @param binder the binder used to get the properties
|
||||||
|
* @return a bound {@link ReadinessProperties} instance
|
||||||
|
*/
|
||||||
|
static ReadinessProperties get(Binder binder) {
|
||||||
|
return binder.bind(ReadinessProperties.NAME, ReadinessProperties.class).orElseGet(ReadinessProperties::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TCP properties.
|
||||||
|
*/
|
||||||
|
public static class Tcp {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeout for connections.
|
||||||
|
*/
|
||||||
|
private Duration connectTimeout = Duration.ofMillis(200);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeout for reads.
|
||||||
|
*/
|
||||||
|
private Duration readTimeout = Duration.ofMillis(200);
|
||||||
|
|
||||||
|
public Duration getConnectTimeout() {
|
||||||
|
return this.connectTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConnectTimeout(Duration connectTimeout) {
|
||||||
|
this.connectTimeout = connectTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Duration getReadTimeout() {
|
||||||
|
return this.readTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReadTimeout(Duration readTimeout) {
|
||||||
|
this.readTimeout = readTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.readiness;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown if readiness checking has timed out. Related
|
||||||
|
* {@link ServiceNotReadyException} are available from {@link #getSuppressed()}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public final class ReadinessTimeoutException extends RuntimeException {
|
||||||
|
|
||||||
|
private final Duration timeout;
|
||||||
|
|
||||||
|
ReadinessTimeoutException(Duration timeout, List<ServiceNotReadyException> exceptions) {
|
||||||
|
super(buildMessage(timeout, exceptions));
|
||||||
|
this.timeout = timeout;
|
||||||
|
exceptions.forEach(this::addSuppressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildMessage(Duration timeout, List<ServiceNotReadyException> exceptions) {
|
||||||
|
List<String> serviceNames = exceptions.stream()
|
||||||
|
.map(ServiceNotReadyException::getService)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(RunningService::name)
|
||||||
|
.toList();
|
||||||
|
return "Readiness timeout of %s reached while waiting for services %s".formatted(timeout, serviceNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the timeout that was reached.
|
||||||
|
* @return the timeout
|
||||||
|
*/
|
||||||
|
public Duration getTimeout() {
|
||||||
|
return this.timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.readiness;
|
||||||
|
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when a single {@link RunningService} is not ready.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
* @see ServiceReadinessCheck
|
||||||
|
*/
|
||||||
|
public class ServiceNotReadyException extends RuntimeException {
|
||||||
|
|
||||||
|
private final RunningService service;
|
||||||
|
|
||||||
|
ServiceNotReadyException(RunningService service, String message) {
|
||||||
|
this(service, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceNotReadyException(RunningService service, String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the service that was not reeady.
|
||||||
|
* @return the non-ready service
|
||||||
|
*/
|
||||||
|
public RunningService getService() {
|
||||||
|
return this.service;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.readiness;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.bind.Binder;
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strategy used to check if a {@link RunningService} is ready. Implementations may be
|
||||||
|
* registered in {@code spring.factories}. The following constructor arguments types are
|
||||||
|
* supported:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link ClassLoader}</li>
|
||||||
|
* <li>{@link Environment}</li>
|
||||||
|
* <li>{@link Binder}</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public interface ServiceReadinessCheck {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the given {@code service} is ready.
|
||||||
|
* @param service service to check
|
||||||
|
* @throws ServiceNotReadyException if the service is not ready
|
||||||
|
*/
|
||||||
|
void check(RunningService service) throws ServiceNotReadyException;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.readiness;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.bind.Binder;
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||||
|
import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver;
|
||||||
|
import org.springframework.core.log.LogMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of {@link ServiceReadinessCheck} instances that can be used to
|
||||||
|
* {@link #wait() wait} for {@link RunningService services} to be ready.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public class ServiceReadinessChecks {
|
||||||
|
|
||||||
|
private static final Log logger = LogFactory.getLog(ServiceReadinessChecks.class);
|
||||||
|
|
||||||
|
private static final String DISABLE_LABEL = "org.springframework.boot.readiness-check.disable";
|
||||||
|
|
||||||
|
private static final Duration SLEEP_BETWEEN_READINESS_TRIES = Duration.ofSeconds(1);
|
||||||
|
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
|
private final Consumer<Duration> sleep;
|
||||||
|
|
||||||
|
private final ReadinessProperties properties;
|
||||||
|
|
||||||
|
private final List<ServiceReadinessCheck> checks;
|
||||||
|
|
||||||
|
public ServiceReadinessChecks(ClassLoader classLoader, Environment environment, Binder binder) {
|
||||||
|
this(Clock.systemUTC(), ServiceReadinessChecks::sleep,
|
||||||
|
SpringFactoriesLoader.forDefaultResourceLocation(classLoader), classLoader, environment, binder,
|
||||||
|
TcpConnectServiceReadinessCheck::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceReadinessChecks(Clock clock, Consumer<Duration> sleep, SpringFactoriesLoader loader, ClassLoader classLoader,
|
||||||
|
Environment environment, Binder binder,
|
||||||
|
Function<ReadinessProperties.Tcp, ServiceReadinessCheck> tcpCheckFactory) {
|
||||||
|
ArgumentResolver argumentResolver = ArgumentResolver.of(ClassLoader.class, classLoader)
|
||||||
|
.and(Environment.class, environment)
|
||||||
|
.and(Binder.class, binder);
|
||||||
|
this.clock = clock;
|
||||||
|
this.sleep = sleep;
|
||||||
|
this.properties = ReadinessProperties.get(binder);
|
||||||
|
this.checks = new ArrayList<>(loader.load(ServiceReadinessCheck.class, argumentResolver));
|
||||||
|
this.checks.add(tcpCheckFactory.apply(this.properties.getTcp()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for the given services to be ready.
|
||||||
|
* @param runningServices the services to wait for
|
||||||
|
*/
|
||||||
|
public void waitUntilReady(List<RunningService> runningServices) {
|
||||||
|
Duration timeout = this.properties.getTimeout();
|
||||||
|
Instant start = this.clock.instant();
|
||||||
|
while (true) {
|
||||||
|
List<ServiceNotReadyException> exceptions = check(runningServices);
|
||||||
|
if (exceptions.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Duration elapsed = Duration.between(start, this.clock.instant());
|
||||||
|
if (elapsed.compareTo(timeout) > 0) {
|
||||||
|
throw new ReadinessTimeoutException(timeout, exceptions);
|
||||||
|
}
|
||||||
|
this.sleep.accept(SLEEP_BETWEEN_READINESS_TRIES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ServiceNotReadyException> check(List<RunningService> runningServices) {
|
||||||
|
List<ServiceNotReadyException> exceptions = null;
|
||||||
|
for (RunningService service : runningServices) {
|
||||||
|
if (isDisabled(service)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
logger.trace(LogMessage.format("Checking readiness of service '%s'", service));
|
||||||
|
for (ServiceReadinessCheck check : this.checks) {
|
||||||
|
try {
|
||||||
|
check.check(service);
|
||||||
|
logger.trace(LogMessage.format("Service '%s' is ready", service));
|
||||||
|
}
|
||||||
|
catch (ServiceNotReadyException ex) {
|
||||||
|
logger.trace(LogMessage.format("Service '%s' is not ready", service), ex);
|
||||||
|
exceptions = (exceptions != null) ? exceptions : new ArrayList<>();
|
||||||
|
exceptions.add(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (exceptions != null) ? exceptions : Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDisabled(RunningService service) {
|
||||||
|
return service.labels().containsKey(DISABLE_LABEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sleep(Duration duration) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(duration.toMillis());
|
||||||
|
}
|
||||||
|
catch (InterruptedException ex) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.readiness;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.SocketTimeoutException;
|
||||||
|
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default {@link ServiceReadinessCheck} that readiness by connecting to the exposed TCP
|
||||||
|
* ports.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class TcpConnectServiceReadinessCheck implements ServiceReadinessCheck {
|
||||||
|
|
||||||
|
private final String DISABLE_LABEL = "org.springframework.boot.readiness-check.tcp.disable";
|
||||||
|
|
||||||
|
private final ReadinessProperties.Tcp properties;
|
||||||
|
|
||||||
|
TcpConnectServiceReadinessCheck(ReadinessProperties.Tcp properties) {
|
||||||
|
this.properties = properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(RunningService service) {
|
||||||
|
if (service.labels().containsKey(this.DISABLE_LABEL)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int port : service.ports().getAll("tcp")) {
|
||||||
|
check(service, port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void check(RunningService service, int port) {
|
||||||
|
int connectTimeout = (int) this.properties.getConnectTimeout().toMillis();
|
||||||
|
int readTimeout = (int) this.properties.getReadTimeout().toMillis();
|
||||||
|
try (Socket socket = new Socket()) {
|
||||||
|
socket.setSoTimeout(readTimeout);
|
||||||
|
socket.connect(new InetSocketAddress(service.host(), port), connectTimeout);
|
||||||
|
check(service, port, socket);
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new ServiceNotReadyException(service, "IOException while connecting to port %s".formatted(port), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void check(RunningService service, int port, Socket socket) throws IOException {
|
||||||
|
try {
|
||||||
|
// -1 is indicates the socket has been closed immediately
|
||||||
|
// Other responses or a timeout are considered as success
|
||||||
|
if (socket.getInputStream().read() == -1) {
|
||||||
|
throw new ServiceNotReadyException(service,
|
||||||
|
"Immediate disconnect while connecting to port %s".formatted(port));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (SocketTimeoutException ex) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service readiness checks.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.docker.compose.readiness;
|
@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
|
||||||
|
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory;
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.boot.origin.Origin;
|
||||||
|
import org.springframework.boot.origin.OriginProvider;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
import org.springframework.util.ObjectUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for {@link ConnectionDetailsFactory} implementations that provide
|
||||||
|
* {@link ConnectionDetails} from a {@link DockerComposeConnectionSource}.
|
||||||
|
*
|
||||||
|
* @param <D> the connection details type
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public abstract class DockerComposeConnectionDetailsFactory<D extends ConnectionDetails>
|
||||||
|
implements ConnectionDetailsFactory<DockerComposeConnectionSource, D> {
|
||||||
|
|
||||||
|
private final String connectionName;
|
||||||
|
|
||||||
|
private final String[] requiredClassNames;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link DockerComposeConnectionDetailsFactory} instance.
|
||||||
|
* @param connectionName the required connection name
|
||||||
|
* @param requiredClassNames the names of classes that must be present
|
||||||
|
*/
|
||||||
|
protected DockerComposeConnectionDetailsFactory(String connectionName, String... requiredClassNames) {
|
||||||
|
this.connectionName = connectionName;
|
||||||
|
this.requiredClassNames = requiredClassNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final D getConnectionDetails(DockerComposeConnectionSource source) {
|
||||||
|
return (!accept(source)) ? null : getDockerComposeConnectionDetails(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean accept(DockerComposeConnectionSource source) {
|
||||||
|
return hasRequiredClasses() && this.connectionName.equals(getConnectionName(source.getRunningService()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getConnectionName(RunningService service) {
|
||||||
|
String connectionName = service.labels().get("org.springframework.boot.service-connection");
|
||||||
|
return (connectionName != null) ? connectionName : service.image().getImageName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasRequiredClasses() {
|
||||||
|
return ObjectUtils.isEmpty(this.requiredClassNames) || Arrays.stream(this.requiredClassNames)
|
||||||
|
.allMatch((requiredClassName) -> ClassUtils.isPresent(requiredClassName, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the {@link ConnectionDetails} from the given {@link RunningService}
|
||||||
|
* {@code source}. May return {@code null} if no connection can be created. Result
|
||||||
|
* types should consider extending {@link DockerComposeConnectionDetails}.
|
||||||
|
* @param source the source
|
||||||
|
* @return the service connection or {@code null}.
|
||||||
|
*/
|
||||||
|
protected abstract D getDockerComposeConnectionDetails(DockerComposeConnectionSource source);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenient base class for {@link ConnectionDetails} results that are backed by a
|
||||||
|
* {@link RunningService}.
|
||||||
|
*/
|
||||||
|
protected static class DockerComposeConnectionDetails implements ConnectionDetails, OriginProvider {
|
||||||
|
|
||||||
|
private final Origin origin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link DockerComposeConnectionDetails} instance.
|
||||||
|
* @param runningService the source {@link RunningService}
|
||||||
|
*/
|
||||||
|
protected DockerComposeConnectionDetails(RunningService runningService) {
|
||||||
|
Assert.notNull(runningService, "RunningService must not be null");
|
||||||
|
this.origin = Origin.from(runningService);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Origin getOrigin() {
|
||||||
|
return this.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection;
|
||||||
|
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passed to {@link DockerComposeConnectionDetailsFactory} to provide details of the
|
||||||
|
* {@link RunningService running docker compose service}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
* @see DockerComposeConnectionDetailsFactory
|
||||||
|
*/
|
||||||
|
public final class DockerComposeConnectionSource {
|
||||||
|
|
||||||
|
private final RunningService runningService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link DockerComposeConnectionSource} instance.
|
||||||
|
* @param runningService the running docker compose service
|
||||||
|
*/
|
||||||
|
DockerComposeConnectionSource(RunningService runningService) {
|
||||||
|
this.runningService = runningService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the running docker compose service.
|
||||||
|
* @return the running service
|
||||||
|
*/
|
||||||
|
public RunningService getRunningService() {
|
||||||
|
return this.runningService;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||||
|
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||||
|
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
|
||||||
|
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories;
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.boot.docker.compose.lifecycle.DockerComposeServicesReadyEvent;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationListener;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ApplicationListener} that listens for an {@link DockerComposeServicesReadyEvent}
|
||||||
|
* in order to establish service connections.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DockerComposeServiceConnectionsApplicationListener
|
||||||
|
implements ApplicationListener<DockerComposeServicesReadyEvent> {
|
||||||
|
|
||||||
|
private final ConnectionDetailsFactories factories;
|
||||||
|
|
||||||
|
DockerComposeServiceConnectionsApplicationListener() {
|
||||||
|
this(new ConnectionDetailsFactories());
|
||||||
|
}
|
||||||
|
|
||||||
|
DockerComposeServiceConnectionsApplicationListener(ConnectionDetailsFactories factories) {
|
||||||
|
this.factories = factories;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplicationEvent(DockerComposeServicesReadyEvent event) {
|
||||||
|
ApplicationContext applicationContext = event.getSource();
|
||||||
|
if (applicationContext instanceof BeanDefinitionRegistry registry) {
|
||||||
|
registerConnectionDetails(registry, event.getRunningServices());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerConnectionDetails(BeanDefinitionRegistry registry, List<RunningService> runningServices) {
|
||||||
|
for (RunningService runningService : runningServices) {
|
||||||
|
DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService);
|
||||||
|
this.factories.getConnectionDetails(source)
|
||||||
|
.forEach((connectionDetailsType, connectionDetails) -> register(registry, runningService,
|
||||||
|
connectionDetailsType, connectionDetails));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <T> void register(BeanDefinitionRegistry registry, RunningService runningService,
|
||||||
|
Class<?> connectionDetailsType, ConnectionDetails connectionDetails) {
|
||||||
|
String beanName = getBeanName(runningService, connectionDetailsType, connectionDetails);
|
||||||
|
Class<T> beanType = (Class<T>) connectionDetails.getClass();
|
||||||
|
Supplier<T> beanSupplier = () -> (T) connectionDetails;
|
||||||
|
registry.registerBeanDefinition(beanName, new RootBeanDefinition(beanType, beanSupplier));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getBeanName(RunningService runningService, Class<?> connectionDetailsType,
|
||||||
|
ConnectionDetails connectionDetails) {
|
||||||
|
List<String> parts = new ArrayList<>();
|
||||||
|
parts.add(ClassUtils.getShortNameAsProperty(connectionDetailsType));
|
||||||
|
parts.add("for");
|
||||||
|
parts.addAll(Arrays.asList(runningService.name().split("-")));
|
||||||
|
return StringUtils.uncapitalize(parts.stream().map(StringUtils::capitalize).collect(Collectors.joining()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.elasticsearch;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails;
|
||||||
|
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node.Protocol;
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DockerComposeConnectionDetailsFactory} to create
|
||||||
|
* {@link ElasticsearchConnectionDetails} for an {@code elasticsearch} service.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class ElasticsearchDockerComposeConnectionDetailsFactory
|
||||||
|
extends DockerComposeConnectionDetailsFactory<ElasticsearchConnectionDetails> {
|
||||||
|
|
||||||
|
private static final int ELASTICSEARCH_PORT = 9200;
|
||||||
|
|
||||||
|
protected ElasticsearchDockerComposeConnectionDetailsFactory(String name) {
|
||||||
|
super("elasticsearch");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ElasticsearchConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
|
||||||
|
return new ElasticsearchDockerComposeConnectionDetails(source.getRunningService());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ElasticsearchConnectionDetails} backed by an {@code elasticsearch}
|
||||||
|
* {@link RunningService}.
|
||||||
|
*/
|
||||||
|
static class ElasticsearchDockerComposeConnectionDetails extends DockerComposeConnectionDetails
|
||||||
|
implements ElasticsearchConnectionDetails {
|
||||||
|
|
||||||
|
private final ElasticsearchEnvironment environment;
|
||||||
|
|
||||||
|
private final List<Node> nodes;
|
||||||
|
|
||||||
|
ElasticsearchDockerComposeConnectionDetails(RunningService service) {
|
||||||
|
super(service);
|
||||||
|
this.environment = new ElasticsearchEnvironment(service.env());
|
||||||
|
this.nodes = List.of(new Node(service.host(), service.ports().get(ELASTICSEARCH_PORT), Protocol.HTTP,
|
||||||
|
getUsername(), getPassword()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsername() {
|
||||||
|
return "elastic";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPassword() {
|
||||||
|
return this.environment.getPassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Node> getNodes() {
|
||||||
|
return this.nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.elasticsearch;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elasticsearch environment details.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class ElasticsearchEnvironment {
|
||||||
|
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
ElasticsearchEnvironment(Map<String, String> env) {
|
||||||
|
Assert.state(!env.containsKey("ELASTIC_PASSWORD_FILE"), "ELASTIC_PASSWORD_FILE is not supported");
|
||||||
|
this.password = env.get("ELASTIC_PASSWORD");
|
||||||
|
}
|
||||||
|
|
||||||
|
String getPassword() {
|
||||||
|
return this.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-configuration for docker compose Elasticsearch service connections.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.elasticsearch;
|
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.jdbc;
|
||||||
|
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility used to build a JDBC URL for a {@link RunningService}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public class JdbcUrlBuilder {
|
||||||
|
|
||||||
|
private static final String PARAMETERS_LABEL = "org.springframework.boot.jdbc.parameters";
|
||||||
|
|
||||||
|
private final String driverProtocol;
|
||||||
|
|
||||||
|
private final int containerPort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link JdbcUrlBuilder} instance.
|
||||||
|
* @param driverProtocol the driver protocol
|
||||||
|
* @param containerPort the source container port
|
||||||
|
*/
|
||||||
|
public JdbcUrlBuilder(String driverProtocol, int containerPort) {
|
||||||
|
Assert.notNull(driverProtocol, "DriverProtocol must not be null");
|
||||||
|
this.driverProtocol = driverProtocol;
|
||||||
|
this.containerPort = containerPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a JDBC URL for the given {@link RunningService} and database.
|
||||||
|
* @param service the running service
|
||||||
|
* @param database the database to connect to
|
||||||
|
* @return a new JDBC URL
|
||||||
|
*/
|
||||||
|
public String build(RunningService service, String database) {
|
||||||
|
Assert.notNull(service, "Service must not be null");
|
||||||
|
Assert.notNull(database, "Database must not be null");
|
||||||
|
String parameters = getParameters(service);
|
||||||
|
return "jdbc:%s://%s:%d/%s%s".formatted(this.driverProtocol, service.host(),
|
||||||
|
service.ports().get(this.containerPort), database, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getParameters(RunningService service) {
|
||||||
|
String parameters = service.labels().get(PARAMETERS_LABEL);
|
||||||
|
return (StringUtils.hasLength(parameters)) ? "?" + parameters : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities to help when creating
|
||||||
|
* {@link org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails}.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.jdbc;
|
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.mariadb;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MariaDB environment details.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class MariaDbEnvironment {
|
||||||
|
|
||||||
|
private final String username;
|
||||||
|
|
||||||
|
private final String password;
|
||||||
|
|
||||||
|
private final String database;
|
||||||
|
|
||||||
|
MariaDbEnvironment(Map<String, String> env) {
|
||||||
|
this.username = extractUsername(env);
|
||||||
|
this.password = extractPassword(env);
|
||||||
|
this.database = extractDatabase(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractUsername(Map<String, String> env) {
|
||||||
|
String user = env.get("MARIADB_USER");
|
||||||
|
return (user != null) ? user : env.getOrDefault("MYSQL_USER", "root");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractPassword(Map<String, String> env) {
|
||||||
|
Assert.state(!env.containsKey("MARIADB_RANDOM_ROOT_PASSWORD"), "MARIADB_RANDOM_ROOT_PASSWORD is not supported");
|
||||||
|
Assert.state(!env.containsKey("MYSQL_RANDOM_ROOT_PASSWORD"), "MYSQL_RANDOM_ROOT_PASSWORD is not supported");
|
||||||
|
Assert.state(!env.containsKey("MARIADB_ROOT_PASSWORD_HASH"), "MARIADB_ROOT_PASSWORD_HASH is not supported");
|
||||||
|
boolean allowEmpty = env.containsKey("MARIADB_ALLOW_EMPTY_PASSWORD")
|
||||||
|
|| env.containsKey("MYSQL_ALLOW_EMPTY_PASSWORD");
|
||||||
|
String password = env.get("MARIADB_PASSWORD");
|
||||||
|
password = (password != null) ? password : env.get("MYSQL_PASSWORD");
|
||||||
|
password = (password != null) ? password : env.get("MARIADB_ROOT_PASSWORD");
|
||||||
|
password = (password != null) ? password : env.get("MYSQL_ROOT_PASSWORD");
|
||||||
|
Assert.state(StringUtils.hasLength(password) || allowEmpty, "No MariaDB password found");
|
||||||
|
return (password != null) ? password : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractDatabase(Map<String, String> env) {
|
||||||
|
String database = env.get("MARIADB_DATABASE");
|
||||||
|
database = (database != null) ? database : env.get("MYSQL_DATABASE");
|
||||||
|
Assert.state(database != null, "No MARIADB_DATABASE defined");
|
||||||
|
return database;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getUsername() {
|
||||||
|
return this.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getPassword() {
|
||||||
|
return this.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getDatabase() {
|
||||||
|
return this.database;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.mariadb;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DockerComposeConnectionDetailsFactory} to create {@link JdbcConnectionDetails}
|
||||||
|
* for a {@code mariadb} service.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class MariaDbJdbcDockerComposeConnectionDetailsFactory
|
||||||
|
extends DockerComposeConnectionDetailsFactory<JdbcConnectionDetails> {
|
||||||
|
|
||||||
|
protected MariaDbJdbcDockerComposeConnectionDetailsFactory() {
|
||||||
|
super("mariadb");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected JdbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
|
||||||
|
return new MariaDbJdbcDockerComposeConnectionDetails(source.getRunningService());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link JdbcConnectionDetails} backed by a {@code mariadb} {@link RunningService}.
|
||||||
|
*/
|
||||||
|
static class MariaDbJdbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails
|
||||||
|
implements JdbcConnectionDetails {
|
||||||
|
|
||||||
|
private static final JdbcUrlBuilder jdbcUrlBuilder = new JdbcUrlBuilder("mariadb", 3306);
|
||||||
|
|
||||||
|
private final MariaDbEnvironment environment;
|
||||||
|
|
||||||
|
private final String jdbcUrl;
|
||||||
|
|
||||||
|
MariaDbJdbcDockerComposeConnectionDetails(RunningService service) {
|
||||||
|
super(service);
|
||||||
|
this.environment = new MariaDbEnvironment(service.env());
|
||||||
|
this.jdbcUrl = jdbcUrlBuilder.build(service, this.environment.getDatabase());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsername() {
|
||||||
|
return this.environment.getUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPassword() {
|
||||||
|
return this.environment.getPassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getJdbcUrl() {
|
||||||
|
return this.jdbcUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.mariadb;
|
||||||
|
|
||||||
|
import io.r2dbc.spi.ConnectionFactoryOptions;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails;
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.r2dbc.ConnectionFactoryOptionsBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DockerComposeConnectionDetailsFactory} to create {@link R2dbcConnectionDetails}
|
||||||
|
* for a {@code mariadb} service.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class MariaDbR2dbcDockerComposeConnectionDetailsFactory
|
||||||
|
extends DockerComposeConnectionDetailsFactory<R2dbcConnectionDetails> {
|
||||||
|
|
||||||
|
MariaDbR2dbcDockerComposeConnectionDetailsFactory() {
|
||||||
|
super("mariadb", "io.r2dbc.spi.ConnectionFactoryOptions");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected R2dbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
|
||||||
|
return new MariaDbR2dbcDockerComposeConnectionDetails(source.getRunningService());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link R2dbcConnectionDetails} backed by a {@code mariadb} {@link RunningService}.
|
||||||
|
*/
|
||||||
|
static class MariaDbR2dbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails
|
||||||
|
implements R2dbcConnectionDetails {
|
||||||
|
|
||||||
|
private static final ConnectionFactoryOptionsBuilder connectionFactoryOptionsBuilder = new ConnectionFactoryOptionsBuilder(
|
||||||
|
"mariadb", 3306);
|
||||||
|
|
||||||
|
private final ConnectionFactoryOptions connectionFactoryOptions;
|
||||||
|
|
||||||
|
MariaDbR2dbcDockerComposeConnectionDetails(RunningService service) {
|
||||||
|
super(service);
|
||||||
|
MariaDbEnvironment environment = new MariaDbEnvironment(service.env());
|
||||||
|
this.connectionFactoryOptions = connectionFactoryOptionsBuilder.build(service, environment.getDatabase(),
|
||||||
|
environment.getUsername(), environment.getPassword());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConnectionFactoryOptions getConnectionFactoryOptions() {
|
||||||
|
return this.connectionFactoryOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-configuration for docker compose MariaDB service connections.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.mariadb;
|
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.mongo;
|
||||||
|
|
||||||
|
import com.mongodb.ConnectionString;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails;
|
||||||
|
import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails;
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DockerComposeConnectionDetailsFactory} to create {@link MongoConnectionDetails}
|
||||||
|
* for a {@code mongo} service.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class MongoDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory<MongoConnectionDetails> {
|
||||||
|
|
||||||
|
private static final int MONGODB_PORT = 27017;
|
||||||
|
|
||||||
|
protected MongoDockerComposeConnectionDetailsFactory() {
|
||||||
|
super("mongo", "com.mongodb.ConnectionString");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected MongoDockerComposeConnectionDetails getDockerComposeConnectionDetails(
|
||||||
|
DockerComposeConnectionSource source) {
|
||||||
|
return new MongoDockerComposeConnectionDetails(source.getRunningService());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ElasticsearchConnectionDetails} backed by a {@code mariadb}
|
||||||
|
* {@link RunningService}.
|
||||||
|
*/
|
||||||
|
static class MongoDockerComposeConnectionDetails extends DockerComposeConnectionDetails
|
||||||
|
implements MongoConnectionDetails {
|
||||||
|
|
||||||
|
private final ConnectionString connectionString;
|
||||||
|
|
||||||
|
MongoDockerComposeConnectionDetails(RunningService service) {
|
||||||
|
super(service);
|
||||||
|
this.connectionString = buildConnectionString(service);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConnectionString buildConnectionString(RunningService service) {
|
||||||
|
MongoEnvironment environment = new MongoEnvironment(service.env());
|
||||||
|
StringBuilder builder = new StringBuilder("mongodb://");
|
||||||
|
if (environment.getUsername() != null) {
|
||||||
|
builder.append(environment.getUsername());
|
||||||
|
builder.append(":");
|
||||||
|
builder.append((environment.getPassword() != null) ? environment.getPassword() : "");
|
||||||
|
builder.append("@");
|
||||||
|
}
|
||||||
|
builder.append(service.host());
|
||||||
|
builder.append(":");
|
||||||
|
builder.append(service.ports().get(MONGODB_PORT));
|
||||||
|
builder.append("/");
|
||||||
|
builder.append((environment.getDatabase() != null) ? environment.getDatabase() : "test");
|
||||||
|
return new ConnectionString(builder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConnectionString getConnectionString() {
|
||||||
|
return this.connectionString;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.mongo;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MongoDB environment details.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class MongoEnvironment {
|
||||||
|
|
||||||
|
private final String username;
|
||||||
|
|
||||||
|
private final String password;
|
||||||
|
|
||||||
|
private final String database;
|
||||||
|
|
||||||
|
MongoEnvironment(Map<String, String> env) {
|
||||||
|
Assert.state(!env.containsKey("MONGO_INITDB_ROOT_USERNAME_FILE"),
|
||||||
|
"MONGO_INITDB_ROOT_USERNAME_FILE is not supported");
|
||||||
|
Assert.state(!env.containsKey("MONGO_INITDB_ROOT_PASSWORD_FILE"),
|
||||||
|
"MONGO_INITDB_ROOT_PASSWORD_FILE is not supported");
|
||||||
|
this.username = env.get("MONGO_INITDB_ROOT_USERNAME");
|
||||||
|
this.password = env.get("MONGO_INITDB_ROOT_PASSWORD");
|
||||||
|
this.database = env.get("MONGO_INITDB_DATABASE");
|
||||||
|
}
|
||||||
|
|
||||||
|
String getUsername() {
|
||||||
|
return this.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getPassword() {
|
||||||
|
return this.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getDatabase() {
|
||||||
|
return this.database;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-configuration for docker compose MongoDB service connections.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.mongo;
|
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.mysql;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySQL environment details.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class MySqlEnvironment {
|
||||||
|
|
||||||
|
private final String username;
|
||||||
|
|
||||||
|
private final String password;
|
||||||
|
|
||||||
|
private final String database;
|
||||||
|
|
||||||
|
MySqlEnvironment(Map<String, String> env) {
|
||||||
|
this.username = env.getOrDefault("MYSQL_USER", "root");
|
||||||
|
this.password = extractPassword(env);
|
||||||
|
this.database = extractDatabase(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractPassword(Map<String, String> env) {
|
||||||
|
Assert.state(!env.containsKey("MYSQL_RANDOM_ROOT_PASSWORD"), "MYSQL_RANDOM_ROOT_PASSWORD is not supported");
|
||||||
|
boolean allowEmpty = env.containsKey("MYSQL_ALLOW_EMPTY_PASSWORD");
|
||||||
|
String password = env.get("MYSQL_PASSWORD");
|
||||||
|
password = (password != null) ? password : env.get("MYSQL_ROOT_PASSWORD");
|
||||||
|
Assert.state(StringUtils.hasLength(password) || allowEmpty, "No MySQL password found");
|
||||||
|
return (password != null) ? password : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractDatabase(Map<String, String> env) {
|
||||||
|
String database = env.get("MYSQL_DATABASE");
|
||||||
|
Assert.state(database != null, "No MYSQL_DATABASE defined");
|
||||||
|
return database;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getUsername() {
|
||||||
|
return this.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getPassword() {
|
||||||
|
return this.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getDatabase() {
|
||||||
|
return this.database;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.mysql;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DockerComposeConnectionDetailsFactory} to create {@link JdbcConnectionDetails}
|
||||||
|
* for a {@code mysql} service.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class MySqlJdbcDockerComposeConnectionDetailsFactory
|
||||||
|
extends DockerComposeConnectionDetailsFactory<JdbcConnectionDetails> {
|
||||||
|
|
||||||
|
protected MySqlJdbcDockerComposeConnectionDetailsFactory() {
|
||||||
|
super("mysql");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected JdbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
|
||||||
|
return new MySqlJdbcDockerComposeConnectionDetails(source.getRunningService());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link JdbcConnectionDetails} backed by a {@code mysql} {@link RunningService}.
|
||||||
|
*/
|
||||||
|
static class MySqlJdbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails
|
||||||
|
implements JdbcConnectionDetails {
|
||||||
|
|
||||||
|
private static final JdbcUrlBuilder jdbcUrlBuilder = new JdbcUrlBuilder("mysql", 3306);
|
||||||
|
|
||||||
|
private final MySqlEnvironment environment;
|
||||||
|
|
||||||
|
private final String jdbcUrl;
|
||||||
|
|
||||||
|
MySqlJdbcDockerComposeConnectionDetails(RunningService service) {
|
||||||
|
super(service);
|
||||||
|
this.environment = new MySqlEnvironment(service.env());
|
||||||
|
this.jdbcUrl = jdbcUrlBuilder.build(service, this.environment.getDatabase());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsername() {
|
||||||
|
return this.environment.getUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPassword() {
|
||||||
|
return this.environment.getPassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getJdbcUrl() {
|
||||||
|
return this.jdbcUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.mysql;
|
||||||
|
|
||||||
|
import io.r2dbc.spi.ConnectionFactoryOptions;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails;
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.r2dbc.ConnectionFactoryOptionsBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DockerComposeConnectionDetailsFactory} to create {@link R2dbcConnectionDetails}
|
||||||
|
* for a {@code mysql} service.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class MySqlR2dbcDockerComposeConnectionDetailsFactory
|
||||||
|
extends DockerComposeConnectionDetailsFactory<R2dbcConnectionDetails> {
|
||||||
|
|
||||||
|
MySqlR2dbcDockerComposeConnectionDetailsFactory() {
|
||||||
|
super("mysql", "io.r2dbc.spi.ConnectionFactoryOptions");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected R2dbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
|
||||||
|
return new MySqlR2dbcDockerComposeConnectionDetails(source.getRunningService());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link R2dbcConnectionDetails} backed by a {@code mysql} {@link RunningService}.
|
||||||
|
*/
|
||||||
|
static class MySqlR2dbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails
|
||||||
|
implements R2dbcConnectionDetails {
|
||||||
|
|
||||||
|
private static final ConnectionFactoryOptionsBuilder connectionFactoryOptionsBuilder = new ConnectionFactoryOptionsBuilder(
|
||||||
|
"mysql", 3306);
|
||||||
|
|
||||||
|
private final ConnectionFactoryOptions connectionFactoryOptions;
|
||||||
|
|
||||||
|
MySqlR2dbcDockerComposeConnectionDetails(RunningService service) {
|
||||||
|
super(service);
|
||||||
|
MySqlEnvironment environment = new MySqlEnvironment(service.env());
|
||||||
|
this.connectionFactoryOptions = connectionFactoryOptionsBuilder.build(service, environment.getDatabase(),
|
||||||
|
environment.getUsername(), environment.getPassword());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConnectionFactoryOptions getConnectionFactoryOptions() {
|
||||||
|
return this.connectionFactoryOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-configuration for docker compose MySQL service connections.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.mysql;
|
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service connection support for Docker Compose.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.docker.compose.service.connection;
|
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.postgres;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Postgres environment details.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class PostgresEnvironment {
|
||||||
|
|
||||||
|
private final String username;
|
||||||
|
|
||||||
|
private final String password;
|
||||||
|
|
||||||
|
private final String database;
|
||||||
|
|
||||||
|
PostgresEnvironment(Map<String, String> env) {
|
||||||
|
this.username = env.getOrDefault("POSTGRES_USER", "postgres");
|
||||||
|
this.password = extractPassword(env);
|
||||||
|
this.database = env.getOrDefault("POSTGRES_DB", this.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractPassword(Map<String, String> env) {
|
||||||
|
String password = env.get("POSTGRES_PASSWORD");
|
||||||
|
Assert.state(StringUtils.hasLength(password), "No POSTGRES_PASSWORD defined");
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getUsername() {
|
||||||
|
return this.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getPassword() {
|
||||||
|
return this.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getDatabase() {
|
||||||
|
return this.database;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.postgres;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DockerComposeConnectionDetailsFactory} to create {@link JdbcConnectionDetails}
|
||||||
|
* for a {@code postgres} service.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class PostgresJdbcDockerComposeConnectionDetailsFactory
|
||||||
|
extends DockerComposeConnectionDetailsFactory<JdbcConnectionDetails> {
|
||||||
|
|
||||||
|
protected PostgresJdbcDockerComposeConnectionDetailsFactory() {
|
||||||
|
super("postgres");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected JdbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
|
||||||
|
return new PostgresJdbcDockerComposeConnectionDetails(source.getRunningService());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link JdbcConnectionDetails} backed by a {@code postgres} {@link RunningService}.
|
||||||
|
*/
|
||||||
|
static class PostgresJdbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails
|
||||||
|
implements JdbcConnectionDetails {
|
||||||
|
|
||||||
|
private static final JdbcUrlBuilder jdbcUrlBuilder = new JdbcUrlBuilder("postgresql", 5432);
|
||||||
|
|
||||||
|
private final PostgresEnvironment environment;
|
||||||
|
|
||||||
|
private final String jdbcUrl;
|
||||||
|
|
||||||
|
PostgresJdbcDockerComposeConnectionDetails(RunningService service) {
|
||||||
|
super(service);
|
||||||
|
this.environment = new PostgresEnvironment(service.env());
|
||||||
|
this.jdbcUrl = jdbcUrlBuilder.build(service, this.environment.getDatabase());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsername() {
|
||||||
|
return this.environment.getUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPassword() {
|
||||||
|
return this.environment.getPassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getJdbcUrl() {
|
||||||
|
return this.jdbcUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.postgres;
|
||||||
|
|
||||||
|
import io.r2dbc.spi.ConnectionFactoryOptions;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails;
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.r2dbc.ConnectionFactoryOptionsBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DockerComposeConnectionDetailsFactory} to create {@link R2dbcConnectionDetails}
|
||||||
|
* for a {@code postgres} service.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class PostgresR2dbcDockerComposeConnectionDetailsFactory
|
||||||
|
extends DockerComposeConnectionDetailsFactory<R2dbcConnectionDetails> {
|
||||||
|
|
||||||
|
PostgresR2dbcDockerComposeConnectionDetailsFactory() {
|
||||||
|
super("postgres", "io.r2dbc.spi.ConnectionFactoryOptions");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected R2dbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
|
||||||
|
return new PostgresDbR2dbcDockerComposeConnectionDetails(source.getRunningService());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link R2dbcConnectionDetails} backed by a {@code postgres} {@link RunningService}.
|
||||||
|
*/
|
||||||
|
static class PostgresDbR2dbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails
|
||||||
|
implements R2dbcConnectionDetails {
|
||||||
|
|
||||||
|
private static final ConnectionFactoryOptionsBuilder connectionFactoryOptionsBuilder = new ConnectionFactoryOptionsBuilder(
|
||||||
|
"postgresql", 5432);
|
||||||
|
|
||||||
|
private final ConnectionFactoryOptions connectionFactoryOptions;
|
||||||
|
|
||||||
|
PostgresDbR2dbcDockerComposeConnectionDetails(RunningService service) {
|
||||||
|
super(service);
|
||||||
|
PostgresEnvironment environment = new PostgresEnvironment(service.env());
|
||||||
|
this.connectionFactoryOptions = connectionFactoryOptionsBuilder.build(service, environment.getDatabase(),
|
||||||
|
environment.getUsername(), environment.getPassword());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConnectionFactoryOptions getConnectionFactoryOptions() {
|
||||||
|
return this.connectionFactoryOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-configuration for docker compose Postgres service connections.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.postgres;
|
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.r2dbc;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import io.r2dbc.spi.ConnectionFactoryOptions;
|
||||||
|
import io.r2dbc.spi.Option;
|
||||||
|
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility used to build an R2DBC {@link ConnectionFactoryOptions} for a
|
||||||
|
* {@link RunningService}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public class ConnectionFactoryOptionsBuilder {
|
||||||
|
|
||||||
|
private static final String PARAMETERS_LABEL = "org.springframework.boot.r2dbc.parameters";
|
||||||
|
|
||||||
|
private String driver;
|
||||||
|
|
||||||
|
private int sourcePort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link JdbcUrlBuilder} instance.
|
||||||
|
* @param driver the driver protocol
|
||||||
|
* @param containerPort the source container port
|
||||||
|
*/
|
||||||
|
public ConnectionFactoryOptionsBuilder(String driver, int containerPort) {
|
||||||
|
Assert.notNull(driver, "Driver must not be null");
|
||||||
|
this.driver = driver;
|
||||||
|
this.sourcePort = containerPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConnectionFactoryOptions build(RunningService service, String database, String user, String password) {
|
||||||
|
Assert.notNull(service, "Service must not be null");
|
||||||
|
Assert.notNull(database, "Database must not be null");
|
||||||
|
ConnectionFactoryOptions.Builder builder = ConnectionFactoryOptions.builder()
|
||||||
|
.option(ConnectionFactoryOptions.DRIVER, this.driver)
|
||||||
|
.option(ConnectionFactoryOptions.HOST, service.host())
|
||||||
|
.option(ConnectionFactoryOptions.PORT, service.ports().get(this.sourcePort))
|
||||||
|
.option(ConnectionFactoryOptions.DATABASE, database);
|
||||||
|
if (StringUtils.hasLength(user)) {
|
||||||
|
builder.option(ConnectionFactoryOptions.USER, user);
|
||||||
|
}
|
||||||
|
if (StringUtils.hasLength(password)) {
|
||||||
|
builder.option(ConnectionFactoryOptions.PASSWORD, password);
|
||||||
|
}
|
||||||
|
applyParameters(service, builder);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyParameters(RunningService service, ConnectionFactoryOptions.Builder builder) {
|
||||||
|
String parameters = service.labels().get(PARAMETERS_LABEL);
|
||||||
|
try {
|
||||||
|
if (StringUtils.hasText(parameters)) {
|
||||||
|
parseParameters(parameters).forEach((name, value) -> builder.option(Option.valueOf(name), value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (RuntimeException ex) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Unable to apply R2DBC label parameters '%s' defined on service %s".formatted(parameters, service));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> parseParameters(String parameters) {
|
||||||
|
Map<String, String> result = new LinkedHashMap<>();
|
||||||
|
for (String parameter : StringUtils.commaDelimitedListToStringArray(parameters)) {
|
||||||
|
String[] parts = parameter.split("=");
|
||||||
|
Assert.state(parts.length == 2, () -> "Unable to parse parameter '%s'".formatted(parameter));
|
||||||
|
result.put(parts[0], parts[1]);
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableMap(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities to help when creating
|
||||||
|
* {@link org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails}.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.r2dbc;
|
@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.rabbit;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails;
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DockerComposeConnectionDetailsFactory} to create {@link RabbitConnectionDetails}
|
||||||
|
* for a {@code rabbitmq} service.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class RabbitDockerComposeConnectionDetailsFactory
|
||||||
|
extends DockerComposeConnectionDetailsFactory<RabbitConnectionDetails> {
|
||||||
|
|
||||||
|
private static final int RABBITMQ_PORT = 5672;
|
||||||
|
|
||||||
|
protected RabbitDockerComposeConnectionDetailsFactory() {
|
||||||
|
super("rabbitmq");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RabbitConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
|
||||||
|
return new RabbitDockerComposeConnectionDetails(source.getRunningService());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link RabbitConnectionDetails} backed by a {@code rabbitmq}
|
||||||
|
* {@link RunningService}.
|
||||||
|
*/
|
||||||
|
static class RabbitDockerComposeConnectionDetails extends DockerComposeConnectionDetails
|
||||||
|
implements RabbitConnectionDetails {
|
||||||
|
|
||||||
|
private final RabbitEnvironment environment;
|
||||||
|
|
||||||
|
private final List<Address> addresses;
|
||||||
|
|
||||||
|
protected RabbitDockerComposeConnectionDetails(RunningService service) {
|
||||||
|
super(service);
|
||||||
|
this.environment = new RabbitEnvironment(service.env());
|
||||||
|
this.addresses = List.of(new Address(service.host(), service.ports().get(RABBITMQ_PORT)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsername() {
|
||||||
|
return this.environment.getUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPassword() {
|
||||||
|
return this.environment.getPassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getVirtualHost() {
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Address> getAddresses() {
|
||||||
|
return this.addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.rabbit;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RabbitMQ environment details.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class RabbitEnvironment {
|
||||||
|
|
||||||
|
private final String username;
|
||||||
|
|
||||||
|
private final String password;
|
||||||
|
|
||||||
|
RabbitEnvironment(Map<String, String> env) {
|
||||||
|
this.username = env.getOrDefault("RABBITMQ_DEFAULT_USER", "guest");
|
||||||
|
this.password = env.getOrDefault("RABBITMQ_DEFAULT_PASS", "guest");
|
||||||
|
}
|
||||||
|
|
||||||
|
String getUsername() {
|
||||||
|
return this.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getPassword() {
|
||||||
|
return this.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-configuration for docker compose RabbitMQ service connections.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.rabbit;
|
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.redis;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails;
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DockerComposeConnectionDetailsFactory} to create {@link RedisConnectionDetails}
|
||||||
|
* for a {@code redis} service.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class RedisDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory<RedisConnectionDetails> {
|
||||||
|
|
||||||
|
private static final int REDIS_PORT = 6379;
|
||||||
|
|
||||||
|
RedisDockerComposeConnectionDetailsFactory() {
|
||||||
|
super("redis");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RedisConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
|
||||||
|
return new RedisDockerComposeConnectionDetails(source.getRunningService());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link RedisConnectionDetails} backed by a {@code redis} {@link RunningService}.
|
||||||
|
*/
|
||||||
|
static class RedisDockerComposeConnectionDetails extends DockerComposeConnectionDetails
|
||||||
|
implements RedisConnectionDetails {
|
||||||
|
|
||||||
|
private final Standalone standalone;
|
||||||
|
|
||||||
|
RedisDockerComposeConnectionDetails(RunningService service) {
|
||||||
|
super(service);
|
||||||
|
this.standalone = Standalone.of(service.host(), service.ports().get(REDIS_PORT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Standalone getStandalone() {
|
||||||
|
return this.standalone;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-configuration for docker compose Redis service connections.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.redis;
|
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.zipkin;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinConnectionDetails;
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
|
||||||
|
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DockerComposeConnectionDetailsFactory} to create {@link ZipkinConnectionDetails}
|
||||||
|
* for a {@code zipkin} service.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class ZipkinDockerComposeConnectionDetailsFactory
|
||||||
|
extends DockerComposeConnectionDetailsFactory<ZipkinConnectionDetails> {
|
||||||
|
|
||||||
|
private static final int ZIPKIN_PORT = 9411;
|
||||||
|
|
||||||
|
ZipkinDockerComposeConnectionDetailsFactory() {
|
||||||
|
super("zipkin", "org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinAutoConfiguration");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ZipkinConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
|
||||||
|
return new ZipkinDockerComposeConnectionDetails(source.getRunningService());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ZipkinConnectionDetails} backed by a {@code zipkin} {@link RunningService}.
|
||||||
|
*/
|
||||||
|
static class ZipkinDockerComposeConnectionDetails extends DockerComposeConnectionDetails
|
||||||
|
implements ZipkinConnectionDetails {
|
||||||
|
|
||||||
|
private final String host;
|
||||||
|
|
||||||
|
private final int port;
|
||||||
|
|
||||||
|
ZipkinDockerComposeConnectionDetails(RunningService source) {
|
||||||
|
super(source);
|
||||||
|
this.host = source.host();
|
||||||
|
this.port = source.ports().get(ZIPKIN_PORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSpanEndpoint() {
|
||||||
|
return "http://" + this.host + ":" + this.port + "/api/v2/spans";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-configuration for docker compose Zipkin service connections.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.docker.compose.service.connection.zipkin;
|
@ -0,0 +1,18 @@
|
|||||||
|
# Application Listeners
|
||||||
|
org.springframework.context.ApplicationListener=\
|
||||||
|
org.springframework.boot.docker.compose.lifecycle.DockerComposeListener,\
|
||||||
|
org.springframework.boot.docker.compose.service.connection.DockerComposeServiceConnectionsApplicationListener
|
||||||
|
|
||||||
|
# Connection Detail Factories
|
||||||
|
org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\
|
||||||
|
org.springframework.boot.docker.compose.service.connection.elasticsearch.ElasticsearchDockerComposeConnectionDetailsFactory,\
|
||||||
|
org.springframework.boot.docker.compose.service.connection.mariadb.MariaDbJdbcDockerComposeConnectionDetailsFactory,\
|
||||||
|
org.springframework.boot.docker.compose.service.connection.mariadb.MariaDbR2dbcDockerComposeConnectionDetailsFactory,\
|
||||||
|
org.springframework.boot.docker.compose.service.connection.mongo.MongoDockerComposeConnectionDetailsFactory,\
|
||||||
|
org.springframework.boot.docker.compose.service.connection.mysql.MySqlJdbcDockerComposeConnectionDetailsFactory,\
|
||||||
|
org.springframework.boot.docker.compose.service.connection.mysql.MySqlR2dbcDockerComposeConnectionDetailsFactory,\
|
||||||
|
org.springframework.boot.docker.compose.service.connection.postgres.PostgresJdbcDockerComposeConnectionDetailsFactory,\
|
||||||
|
org.springframework.boot.docker.compose.service.connection.postgres.PostgresR2dbcDockerComposeConnectionDetailsFactory,\
|
||||||
|
org.springframework.boot.docker.compose.service.connection.rabbit.RabbitDockerComposeConnectionDetailsFactory,\
|
||||||
|
org.springframework.boot.docker.compose.service.connection.redis.RedisDockerComposeConnectionDetailsFactory,\
|
||||||
|
org.springframework.boot.docker.compose.service.connection.zipkin.ZipkinDockerComposeConnectionDetailsFactory
|
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.docker.compose.core.DefaultConnectionPorts.ContainerPort;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
|
import static org.assertj.core.api.Assertions.entry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DefaultConnectionPorts}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DefaultConnectionPortsTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createWhenBridgeNetwork() throws IOException {
|
||||||
|
DefaultConnectionPorts ports = createForJson("docker-inspect-bridge-network.json");
|
||||||
|
assertThat(ports.getMappings()).containsExactly(entry(new ContainerPort(6379, "tcp"), 32770));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createWhenHostNetwork() throws Exception {
|
||||||
|
DefaultConnectionPorts ports = createForJson("docker-inspect-host-network.json");
|
||||||
|
assertThat(ports.getMappings()).containsExactly(entry(new ContainerPort(6379, "tcp"), 6379));
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultConnectionPorts createForJson(String path) throws IOException {
|
||||||
|
String json = new ClassPathResource(path, getClass()).getContentAsString(StandardCharsets.UTF_8);
|
||||||
|
DockerCliInspectResponse inspectResponse = DockerJson.deserialize(json, DockerCliInspectResponse.class);
|
||||||
|
return new DefaultConnectionPorts(inspectResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class ContainerPortTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parse() {
|
||||||
|
ContainerPort port = ContainerPort.parse("123/tcp");
|
||||||
|
assertThat(port).isEqualTo(new ContainerPort(123, "tcp"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseWhenNoSlashThrowsException() {
|
||||||
|
assertThatIllegalStateException().isThrownBy(() -> ContainerPort.parse("123"))
|
||||||
|
.withMessage("Unable to parse container port '123'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseWhenMultipleSlashesThrowsException() {
|
||||||
|
assertThatIllegalStateException().isThrownBy(() -> ContainerPort.parse("123/tcp/ip"))
|
||||||
|
.withMessage("Unable to parse container port '123/tcp/ip'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseWhenNotNumberThrowsException() {
|
||||||
|
assertThatIllegalStateException().isThrownBy(() -> ContainerPort.parse("tcp/123"))
|
||||||
|
.withMessage("Unable to parse container port 'tcp/123'");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,163 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.Config;
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.ExposedPort;
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostConfig;
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.NetworkSettings;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.entry;
|
||||||
|
import static org.mockito.BDDMockito.then;
|
||||||
|
import static org.mockito.BDDMockito.willReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DefaultDockerCompose}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DefaultDockerComposeTests {
|
||||||
|
|
||||||
|
private static final String HOST = "192.168.1.1";
|
||||||
|
|
||||||
|
private DockerCli cli = mock(DockerCli.class);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void upRunsUpCommand() {
|
||||||
|
DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST);
|
||||||
|
compose.up();
|
||||||
|
then(this.cli).should().run(new DockerCliCommand.ComposeUp());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void downRunsDownCommand() {
|
||||||
|
DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST);
|
||||||
|
Duration timeout = Duration.ofSeconds(1);
|
||||||
|
compose.down(timeout);
|
||||||
|
then(this.cli).should().run(new DockerCliCommand.ComposeDown(timeout));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startRunsStartCommand() {
|
||||||
|
DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST);
|
||||||
|
compose.start();
|
||||||
|
then(this.cli).should().run(new DockerCliCommand.ComposeStart());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stopRunsStopCommand() {
|
||||||
|
DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST);
|
||||||
|
Duration timeout = Duration.ofSeconds(1);
|
||||||
|
compose.stop(timeout);
|
||||||
|
then(this.cli).should().run(new DockerCliCommand.ComposeStop(timeout));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hasDefinedServicesWhenComposeConfigServicesIsEmptyReturnsFalse() {
|
||||||
|
willReturn(new DockerCliComposeConfigResponse("test", Collections.emptyMap())).given(this.cli)
|
||||||
|
.run(new DockerCliCommand.ComposeConfig());
|
||||||
|
DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST);
|
||||||
|
assertThat(compose.hasDefinedServices()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hasDefinedServicesWhenComposeConfigServicesIsNotEmptyReturnsTrue() {
|
||||||
|
willReturn(new DockerCliComposeConfigResponse("test",
|
||||||
|
Map.of("redis", new DockerCliComposeConfigResponse.Service("redis"))))
|
||||||
|
.given(this.cli)
|
||||||
|
.run(new DockerCliCommand.ComposeConfig());
|
||||||
|
DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST);
|
||||||
|
assertThat(compose.hasDefinedServices()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hasRunningServicesWhenPsListsRunningServiceReturnsTrue() {
|
||||||
|
willReturn(List.of(new DockerCliComposePsResponse("id", "name", "image", "exited"),
|
||||||
|
new DockerCliComposePsResponse("id", "name", "image", "running")))
|
||||||
|
.given(this.cli)
|
||||||
|
.run(new DockerCliCommand.ComposePs());
|
||||||
|
DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST);
|
||||||
|
assertThat(compose.hasRunningServices()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hasRunningServicesWhenPsListReturnsAllExitedReturnsFalse() {
|
||||||
|
willReturn(List.of(new DockerCliComposePsResponse("id", "name", "image", "exited"),
|
||||||
|
new DockerCliComposePsResponse("id", "name", "image", "running")))
|
||||||
|
.given(this.cli)
|
||||||
|
.run(new DockerCliCommand.ComposePs());
|
||||||
|
DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST);
|
||||||
|
assertThat(compose.hasRunningServices()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getRunningServicesReturnsServices() {
|
||||||
|
String id = "123";
|
||||||
|
DockerCliComposePsResponse psResponse = new DockerCliComposePsResponse(id, "name", "redis", "running");
|
||||||
|
Map<String, ExposedPort> exposedPorts = Collections.emptyMap();
|
||||||
|
Config config = new Config("redis", Map.of("spring", "boot"), exposedPorts, List.of("a=b"));
|
||||||
|
NetworkSettings networkSettings = null;
|
||||||
|
HostConfig hostConfig = null;
|
||||||
|
DockerCliInspectResponse inspectResponse = new DockerCliInspectResponse(id, config, networkSettings,
|
||||||
|
hostConfig);
|
||||||
|
willReturn(List.of(psResponse)).given(this.cli).run(new DockerCliCommand.ComposePs());
|
||||||
|
willReturn(List.of(inspectResponse)).given(this.cli).run(new DockerCliCommand.Inspect(List.of(id)));
|
||||||
|
DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST);
|
||||||
|
List<RunningService> runningServices = compose.getRunningServices();
|
||||||
|
assertThat(runningServices).hasSize(1);
|
||||||
|
RunningService runningService = runningServices.get(0);
|
||||||
|
assertThat(runningService.name()).isEqualTo("name");
|
||||||
|
assertThat(runningService.image()).hasToString("redis");
|
||||||
|
assertThat(runningService.host()).isEqualTo(HOST);
|
||||||
|
assertThat(runningService.ports().getAll()).isEmpty();
|
||||||
|
assertThat(runningService.env()).containsExactly(entry("a", "b"));
|
||||||
|
assertThat(runningService.labels()).containsExactly(entry("spring", "boot"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getRunningServicesWhenNoHostUsesHostFromContext() {
|
||||||
|
String id = "123";
|
||||||
|
DockerCliComposePsResponse psResponse = new DockerCliComposePsResponse(id, "name", "redis", "running");
|
||||||
|
Map<String, ExposedPort> exposedPorts = Collections.emptyMap();
|
||||||
|
Config config = new Config("redis", Map.of("spring", "boot"), exposedPorts, List.of("a=b"));
|
||||||
|
NetworkSettings networkSettings = null;
|
||||||
|
HostConfig hostConfig = null;
|
||||||
|
DockerCliInspectResponse inspectResponse = new DockerCliInspectResponse(id, config, networkSettings,
|
||||||
|
hostConfig);
|
||||||
|
willReturn(List.of(new DockerCliContextResponse("test", true, "https://192.168.1.1"))).given(this.cli)
|
||||||
|
.run(new DockerCliCommand.Context());
|
||||||
|
willReturn(List.of(psResponse)).given(this.cli).run(new DockerCliCommand.ComposePs());
|
||||||
|
willReturn(List.of(inspectResponse)).given(this.cli).run(new DockerCliCommand.Inspect(List.of(id)));
|
||||||
|
DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, null);
|
||||||
|
List<RunningService> runningServices = compose.getRunningServices();
|
||||||
|
assertThat(runningServices).hasSize(1);
|
||||||
|
RunningService runningService = runningServices.get(0);
|
||||||
|
assertThat(runningService.host()).isEqualTo("192.168.1.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.Config;
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.ExposedPort;
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostConfig;
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostPort;
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.NetworkSettings;
|
||||||
|
import org.springframework.boot.origin.Origin;
|
||||||
|
import org.springframework.util.FileCopyUtils;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.entry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DefaultRunningService}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DefaultRunningServiceTests {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
File temp;
|
||||||
|
|
||||||
|
private DefaultRunningService runningService;
|
||||||
|
|
||||||
|
private DockerComposeFile composeFile;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() throws Exception {
|
||||||
|
this.composeFile = createComposeFile();
|
||||||
|
DockerHost host = DockerHost.get("192.168.1.1", () -> Collections.emptyList());
|
||||||
|
String id = "123";
|
||||||
|
String name = "my-service";
|
||||||
|
String image = "redis";
|
||||||
|
String state = "running";
|
||||||
|
DockerCliComposePsResponse psResponse = new DockerCliComposePsResponse(id, name, image, state);
|
||||||
|
Map<String, String> labels = Map.of("spring", "boot");
|
||||||
|
Map<String, ExposedPort> exposedPorts = Map.of("8080/tcp", new ExposedPort());
|
||||||
|
List<String> env = List.of("a=b");
|
||||||
|
Config config = new Config(image, labels, exposedPorts, env);
|
||||||
|
Map<String, List<HostPort>> ports = Map.of("8080/tcp", List.of(new HostPort(null, "9090")));
|
||||||
|
NetworkSettings networkSettings = new NetworkSettings(ports);
|
||||||
|
HostConfig hostConfig = new HostConfig("bridge");
|
||||||
|
DockerCliInspectResponse inspectResponse = new DockerCliInspectResponse(id, config, networkSettings,
|
||||||
|
hostConfig);
|
||||||
|
this.runningService = new DefaultRunningService(host, this.composeFile, psResponse, inspectResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DockerComposeFile createComposeFile() throws IOException {
|
||||||
|
File file = new File(this.temp, "compose.yaml");
|
||||||
|
FileCopyUtils.copy(new byte[0], file);
|
||||||
|
return DockerComposeFile.of(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getOriginReturnsOrigin() {
|
||||||
|
assertThat(Origin.from(this.runningService)).isEqualTo(new DockerComposeOrigin(this.composeFile, "my-service"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nameReturnsNameFromPsResponse() {
|
||||||
|
assertThat(this.runningService.name()).isEqualTo("my-service");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void imageReturnsImageFromPsResponse() {
|
||||||
|
assertThat(this.runningService.image()).hasToString("redis");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hostReturnsHost() {
|
||||||
|
assertThat(this.runningService.host()).isEqualTo("192.168.1.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void portsReturnsPortsFromInspectResponse() {
|
||||||
|
ConnectionPorts ports = this.runningService.ports();
|
||||||
|
assertThat(ports.getAll("tcp")).containsExactly(9090);
|
||||||
|
assertThat(ports.get(8080)).isEqualTo(9090);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void envReturnsEnvFromInspectResponse() {
|
||||||
|
assertThat(this.runningService.env()).containsExactly(entry("a", "b"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void labelReturnsLabelsFromInspectResponse() {
|
||||||
|
assertThat(this.runningService.labels()).containsExactly(entry("spring", "boot"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void toStringReturnsServiceName() {
|
||||||
|
assertThat(this.runningService).hasToString("my-service");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DockerCliCommand}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DockerCliCommandTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void context() {
|
||||||
|
DockerCliCommand<?> command = new DockerCliCommand.Context();
|
||||||
|
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER);
|
||||||
|
assertThat(command.getCommand()).containsExactly("context", "ls", "--format={{ json . }}");
|
||||||
|
assertThat(command.deserialize("[]")).isInstanceOf(List.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void inspect() {
|
||||||
|
DockerCliCommand<?> command = new DockerCliCommand.Inspect(List.of("123", "345"));
|
||||||
|
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER);
|
||||||
|
assertThat(command.getCommand()).containsExactly("inspect", "--format={{ json . }}", "123", "345");
|
||||||
|
assertThat(command.deserialize("[]")).isInstanceOf(List.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void composeConfig() {
|
||||||
|
DockerCliCommand<?> command = new DockerCliCommand.ComposeConfig();
|
||||||
|
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
|
||||||
|
assertThat(command.getCommand()).containsExactly("config", "--format=json");
|
||||||
|
assertThat(command.deserialize("{}")).isInstanceOf(DockerCliComposeConfigResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void composePs() {
|
||||||
|
DockerCliCommand<?> command = new DockerCliCommand.ComposePs();
|
||||||
|
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
|
||||||
|
assertThat(command.getCommand()).containsExactly("ps", "--format=json");
|
||||||
|
assertThat(command.deserialize("[]")).isInstanceOf(List.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void composeUp() {
|
||||||
|
DockerCliCommand<?> command = new DockerCliCommand.ComposeUp();
|
||||||
|
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
|
||||||
|
assertThat(command.getCommand()).containsExactly("up", "--no-color", "--quiet-pull", "--detach", "--wait");
|
||||||
|
assertThat(command.deserialize("[]")).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void composeDown() {
|
||||||
|
DockerCliCommand<?> command = new DockerCliCommand.ComposeDown(Duration.ofSeconds(1));
|
||||||
|
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
|
||||||
|
assertThat(command.getCommand()).containsExactly("down", "--timeout", "1");
|
||||||
|
assertThat(command.deserialize("[]")).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void composeStart() {
|
||||||
|
DockerCliCommand<?> command = new DockerCliCommand.ComposeStart();
|
||||||
|
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
|
||||||
|
assertThat(command.getCommand()).containsExactly("start", "--no-color", "--quiet-pull", "--detach", "--wait");
|
||||||
|
assertThat(command.deserialize("[]")).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void composeStop() {
|
||||||
|
DockerCliCommand<?> command = new DockerCliCommand.ComposeStop(Duration.ofSeconds(1));
|
||||||
|
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
|
||||||
|
assertThat(command.getCommand()).containsExactly("stop", "--timeout", "1");
|
||||||
|
assertThat(command.deserialize("[]")).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliComposeConfigResponse.Service;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DockerCliComposeConfigResponse}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DockerCliComposeConfigResponseTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deserializeJson() throws IOException {
|
||||||
|
String json = new ClassPathResource("docker-compose-config.json", getClass())
|
||||||
|
.getContentAsString(StandardCharsets.UTF_8);
|
||||||
|
DockerCliComposeConfigResponse response = DockerJson.deserialize(json, DockerCliComposeConfigResponse.class);
|
||||||
|
DockerCliComposeConfigResponse expected = new DockerCliComposeConfigResponse("redis-docker",
|
||||||
|
Map.of("redis", new Service("redis:7.0")));
|
||||||
|
assertThat(response).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DockerCliComposePsResponse}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DockerCliComposePsResponseTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deserializeJson() throws IOException {
|
||||||
|
String json = new ClassPathResource("docker-compose-ps.json", getClass())
|
||||||
|
.getContentAsString(StandardCharsets.UTF_8);
|
||||||
|
DockerCliComposePsResponse response = DockerJson.deserialize(json, DockerCliComposePsResponse.class);
|
||||||
|
DockerCliComposePsResponse expected = new DockerCliComposePsResponse("f5af31dae7f6", "redis-docker-redis-1",
|
||||||
|
"redis:7.0", "running");
|
||||||
|
assertThat(response).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DockerCliComposeVersionResponse}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DockerCliComposeVersionResponseTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deserializeJson() throws IOException {
|
||||||
|
String json = new ClassPathResource("docker-compose-version.json", getClass())
|
||||||
|
.getContentAsString(StandardCharsets.UTF_8);
|
||||||
|
DockerCliComposeVersionResponse response = DockerJson.deserialize(json, DockerCliComposeVersionResponse.class);
|
||||||
|
DockerCliComposeVersionResponse expected = new DockerCliComposeVersionResponse("123");
|
||||||
|
assertThat(response).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DockerCliContextResponse}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DockerCliContextResponseTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deserializeJson() throws IOException {
|
||||||
|
String json = new ClassPathResource("docker-context.json", getClass())
|
||||||
|
.getContentAsString(StandardCharsets.UTF_8);
|
||||||
|
DockerCliContextResponse response = DockerJson.deserialize(json, DockerCliContextResponse.class);
|
||||||
|
DockerCliContextResponse expected = new DockerCliContextResponse("default", true,
|
||||||
|
"unix:///var/run/docker.sock");
|
||||||
|
assertThat(response).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.Config;
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.ExposedPort;
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostConfig;
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostPort;
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.NetworkSettings;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DockerCliInspectResponse}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DockerCliInspectResponseTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deserializeJson() throws IOException {
|
||||||
|
String json = new ClassPathResource("docker-inspect.json", getClass())
|
||||||
|
.getContentAsString(StandardCharsets.UTF_8);
|
||||||
|
DockerCliInspectResponse response = DockerJson.deserialize(json, DockerCliInspectResponse.class);
|
||||||
|
LinkedHashMap<String, String> expectedLabels = linkedMapOf("com.docker.compose.config-hash",
|
||||||
|
"cfdc8e119d85a53c7d47edb37a3b160a8c83ba48b0428ebc07713befec991dd0",
|
||||||
|
"com.docker.compose.container-number", "1", "com.docker.compose.depends_on", "",
|
||||||
|
"com.docker.compose.image", "sha256:e79ba23ed43baa22054741136bf45bdb041824f41c5e16c0033ea044ca164b82",
|
||||||
|
"com.docker.compose.oneoff", "False", "com.docker.compose.project", "redis-docker",
|
||||||
|
"com.docker.compose.project.config_files", "compose.yaml", "com.docker.compose.project.working_dir",
|
||||||
|
"/", "com.docker.compose.service", "redis", "com.docker.compose.version", "2.16.0");
|
||||||
|
List<String> expectedEnv = List.of("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||||
|
"GOSU_VERSION=1.16", "REDIS_VERSION=7.0.8");
|
||||||
|
Config expectedConfig = new Config("redis:7.0", expectedLabels, Map.of("6379/tcp", new ExposedPort()),
|
||||||
|
expectedEnv);
|
||||||
|
NetworkSettings expectedNetworkSettings = new NetworkSettings(
|
||||||
|
Map.of("6379/tcp", List.of(new HostPort("0.0.0.0", "32770"), new HostPort("::", "32770"))));
|
||||||
|
DockerCliInspectResponse expected = new DockerCliInspectResponse(
|
||||||
|
"f5af31dae7f665bd194ec7261bdc84e5df9c64753abb4a6cec6c33f7cf64c3fc", expectedConfig,
|
||||||
|
expectedNetworkSettings, new HostConfig("redis-docker_default"));
|
||||||
|
assertThat(response).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <K, V> LinkedHashMap<K, V> linkedMapOf(Object... values) {
|
||||||
|
LinkedHashMap<K, V> result = new LinkedHashMap<>();
|
||||||
|
for (int i = 0; i < values.length; i = i + 2) {
|
||||||
|
result.put((K) values[i], (V) values[i + 1]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.testsupport.process.DisabledIfProcessUnavailable;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DockerCli}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
@DisabledIfProcessUnavailable({ "docker", "compose" })
|
||||||
|
class DockerCliTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void runBasicCommand() {
|
||||||
|
DockerCli cli = new DockerCli(null, null, Collections.emptySet());
|
||||||
|
List<DockerCliContextResponse> context = cli.run(new DockerCliCommand.Context());
|
||||||
|
assertThat(context).isNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import org.springframework.util.FileCopyUtils;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DockerComposeFile}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DockerComposeFileTests {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
File temp;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hashCodeAndEquals() throws Exception {
|
||||||
|
File f1 = new File(this.temp, "compose.yml");
|
||||||
|
File f2 = new File(this.temp, "docker-compose.yml");
|
||||||
|
FileCopyUtils.copy(new byte[0], f1);
|
||||||
|
FileCopyUtils.copy(new byte[0], f2);
|
||||||
|
DockerComposeFile c1 = DockerComposeFile.of(f1);
|
||||||
|
DockerComposeFile c2 = DockerComposeFile.of(f1);
|
||||||
|
DockerComposeFile c3 = DockerComposeFile.find(f1.getParentFile());
|
||||||
|
DockerComposeFile c4 = DockerComposeFile.of(f2);
|
||||||
|
assertThat(c1.hashCode()).isEqualTo(c2.hashCode()).isEqualTo(c3.hashCode());
|
||||||
|
assertThat(c1).isEqualTo(c1).isEqualTo(c2).isEqualTo(c3).isNotEqualTo(c4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void toStringReturnsFileName() throws Exception {
|
||||||
|
DockerComposeFile composeFile = createComposeFile("compose.yml");
|
||||||
|
assertThat(composeFile.toString()).endsWith("/compose.yml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findFindsSingleFile() throws Exception {
|
||||||
|
File file = new File(this.temp, "docker-compose.yml");
|
||||||
|
FileCopyUtils.copy(new byte[0], file);
|
||||||
|
DockerComposeFile composeFile = DockerComposeFile.find(file.getParentFile());
|
||||||
|
assertThat(composeFile.toString()).endsWith("/docker-compose.yml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findWhenMultipleFilesPicksBest() throws Exception {
|
||||||
|
File f1 = new File(this.temp, "docker-compose.yml");
|
||||||
|
FileCopyUtils.copy(new byte[0], f1);
|
||||||
|
File f2 = new File(this.temp, "compose.yml");
|
||||||
|
FileCopyUtils.copy(new byte[0], f2);
|
||||||
|
DockerComposeFile composeFile = DockerComposeFile.find(f1.getParentFile());
|
||||||
|
assertThat(composeFile.toString()).endsWith("/compose.yml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findWhenNoComposeFilesReturnsNull() throws Exception {
|
||||||
|
File file = new File(this.temp, "not-a-compose.yml");
|
||||||
|
FileCopyUtils.copy(new byte[0], file);
|
||||||
|
DockerComposeFile composeFile = DockerComposeFile.find(file.getParentFile());
|
||||||
|
assertThat(composeFile).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findWhenWorkingDirectoryDoesNotExistReturnsNull() {
|
||||||
|
File directory = new File(this.temp, "missing");
|
||||||
|
DockerComposeFile composeFile = DockerComposeFile.find(directory);
|
||||||
|
assertThat(composeFile).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findWhenWorkingDirectoryIsNotDirectoryThrowsException() throws Exception {
|
||||||
|
File file = new File(this.temp, "iamafile");
|
||||||
|
FileCopyUtils.copy(new byte[0], file);
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> DockerComposeFile.find(file))
|
||||||
|
.withMessageEndingWith("is not a directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ofReturnsDockerComposeFile() throws Exception {
|
||||||
|
File file = new File(this.temp, "anyfile.yml");
|
||||||
|
FileCopyUtils.copy(new byte[0], file);
|
||||||
|
DockerComposeFile composeFile = DockerComposeFile.of(file);
|
||||||
|
assertThat(composeFile).isNotNull();
|
||||||
|
assertThat(composeFile.toString()).isEqualTo(file.getCanonicalPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ofWhenFileIsNullThrowsException() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> DockerComposeFile.of(null))
|
||||||
|
.withMessage("File must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ofWhenFileDoesNotExistThrowsException() {
|
||||||
|
File file = new File(this.temp, "missing");
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> DockerComposeFile.of(file))
|
||||||
|
.withMessageEndingWith("does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ofWhenFileIsNotFileThrowsException() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> DockerComposeFile.of(this.temp))
|
||||||
|
.withMessageEndingWith("is not a file");
|
||||||
|
}
|
||||||
|
|
||||||
|
private DockerComposeFile createComposeFile(String name) throws IOException {
|
||||||
|
File file = new File(this.temp, name);
|
||||||
|
FileCopyUtils.copy(new byte[0], file);
|
||||||
|
return DockerComposeFile.of(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import org.springframework.util.FileCopyUtils;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DockerComposeOrigin}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DockerComposeOriginTests {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
File temp;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hasToString() throws Exception {
|
||||||
|
DockerComposeFile composeFile = createTempComposeFile();
|
||||||
|
DockerComposeOrigin origin = new DockerComposeOrigin(composeFile, "service-1");
|
||||||
|
assertThat(origin.toString()).startsWith("Docker compose service 'service-1' defined in '")
|
||||||
|
.endsWith("compose.yaml'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void equalsAndHashcode() throws Exception {
|
||||||
|
DockerComposeFile composeFile = createTempComposeFile();
|
||||||
|
DockerComposeOrigin origin1 = new DockerComposeOrigin(composeFile, "service-1");
|
||||||
|
DockerComposeOrigin origin2 = new DockerComposeOrigin(composeFile, "service-1");
|
||||||
|
DockerComposeOrigin origin3 = new DockerComposeOrigin(composeFile, "service-3");
|
||||||
|
assertThat(origin1).isEqualTo(origin1);
|
||||||
|
assertThat(origin1).isEqualTo(origin2);
|
||||||
|
assertThat(origin1).hasSameHashCodeAs(origin2);
|
||||||
|
assertThat(origin2).isEqualTo(origin1);
|
||||||
|
assertThat(origin1).isNotEqualTo(origin3);
|
||||||
|
assertThat(origin2).isNotEqualTo(origin3);
|
||||||
|
assertThat(origin3).isNotEqualTo(origin1);
|
||||||
|
assertThat(origin3).isNotEqualTo(origin2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DockerComposeFile createTempComposeFile() throws IOException {
|
||||||
|
File file = new File(this.temp, "compose.yaml");
|
||||||
|
FileCopyUtils.copy(new byte[0], file);
|
||||||
|
return DockerComposeFile.of(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.entry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DockerEnv}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DockerEnvTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createWhenEnvIsNullReturnsEmpty() {
|
||||||
|
DockerEnv env = new DockerEnv(null);
|
||||||
|
assertThat(env.asMap()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createWhenEnvIsEmptyReturnsEmpty() {
|
||||||
|
DockerEnv env = new DockerEnv(Collections.emptyList());
|
||||||
|
assertThat(env.asMap()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createParsesEnv() {
|
||||||
|
DockerEnv env = new DockerEnv(List.of("a=b", "c"));
|
||||||
|
assertThat(env.asMap()).containsExactly(entry("a", "b"), entry("c", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,184 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DockerHost}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DockerHostTests {
|
||||||
|
|
||||||
|
private static final String MAC_HOST = "unix:///var/run/docker.sock";
|
||||||
|
|
||||||
|
private static final String LINUX_HOST = "unix:///var/run/docker.sock";
|
||||||
|
|
||||||
|
private static final String WINDOWS_HOST = "npipe:////./pipe/docker_engine";
|
||||||
|
|
||||||
|
private static final String WSL_HOST = "unix:///var/run/docker.sock";
|
||||||
|
|
||||||
|
private static final String HTTP_HOST = "http://192.168.1.1";
|
||||||
|
|
||||||
|
private static final String HTTPS_HOST = "https://192.168.1.1";
|
||||||
|
|
||||||
|
private static final String TCP_HOST = "tcp://192.168.1.1";
|
||||||
|
|
||||||
|
private static final Function<String, String> NO_SYSTEM_ENV = (key) -> null;
|
||||||
|
|
||||||
|
private static final Supplier<List<DockerCliContextResponse>> NO_CONTEXT = () -> Collections.emptyList();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWhenHasHost() {
|
||||||
|
DockerHost host = DockerHost.get("192.168.1.1", NO_SYSTEM_ENV, NO_CONTEXT);
|
||||||
|
assertThat(host).hasToString("192.168.1.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWhenHasServiceHostEnv() {
|
||||||
|
Map<String, String> systemEnv = Map.of("SERVICES_HOST", "192.168.1.2");
|
||||||
|
DockerHost host = DockerHost.get(null, systemEnv::get, NO_CONTEXT);
|
||||||
|
assertThat(host).hasToString("192.168.1.2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWhenHasMacDockerHostEnv() {
|
||||||
|
Map<String, String> systemEnv = Map.of("DOCKER_HOST", MAC_HOST);
|
||||||
|
DockerHost host = DockerHost.get(null, systemEnv::get, NO_CONTEXT);
|
||||||
|
assertThat(host).hasToString("127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWhenHasLinuxDockerHostEnv() {
|
||||||
|
Map<String, String> systemEnv = Map.of("DOCKER_HOST", LINUX_HOST);
|
||||||
|
DockerHost host = DockerHost.get(null, systemEnv::get, NO_CONTEXT);
|
||||||
|
assertThat(host).hasToString("127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWhenHasWindowsDockerHostEnv() {
|
||||||
|
Map<String, String> systemEnv = Map.of("DOCKER_HOST", WINDOWS_HOST);
|
||||||
|
DockerHost host = DockerHost.get(null, systemEnv::get, NO_CONTEXT);
|
||||||
|
assertThat(host).hasToString("127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWhenHasWslDockerHostEnv() {
|
||||||
|
Map<String, String> systemEnv = Map.of("DOCKER_HOST", WSL_HOST);
|
||||||
|
DockerHost host = DockerHost.get(null, systemEnv::get, NO_CONTEXT);
|
||||||
|
assertThat(host).hasToString("127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWhenHasHttpDockerHostEnv() {
|
||||||
|
Map<String, String> systemEnv = Map.of("DOCKER_HOST", HTTP_HOST);
|
||||||
|
DockerHost host = DockerHost.get(null, systemEnv::get, NO_CONTEXT);
|
||||||
|
assertThat(host).hasToString("192.168.1.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWhenHasHttpsDockerHostEnv() {
|
||||||
|
Map<String, String> systemEnv = Map.of("DOCKER_HOST", HTTPS_HOST);
|
||||||
|
DockerHost host = DockerHost.get(null, systemEnv::get, NO_CONTEXT);
|
||||||
|
assertThat(host).hasToString("192.168.1.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWhenHasTcpDockerHostEnv() {
|
||||||
|
Map<String, String> systemEnv = Map.of("DOCKER_HOST", TCP_HOST);
|
||||||
|
DockerHost host = DockerHost.get(null, systemEnv::get, NO_CONTEXT);
|
||||||
|
assertThat(host).hasToString("192.168.1.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWhenHasMacContext() {
|
||||||
|
List<DockerCliContextResponse> context = List.of(new DockerCliContextResponse("test", true, MAC_HOST));
|
||||||
|
DockerHost host = DockerHost.get(null, NO_SYSTEM_ENV, () -> context);
|
||||||
|
assertThat(host).hasToString("127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWhenHasLinuxContext() {
|
||||||
|
List<DockerCliContextResponse> context = List.of(new DockerCliContextResponse("test", true, LINUX_HOST));
|
||||||
|
DockerHost host = DockerHost.get(null, NO_SYSTEM_ENV, () -> context);
|
||||||
|
assertThat(host).hasToString("127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWhenHasWindowsContext() {
|
||||||
|
List<DockerCliContextResponse> context = List.of(new DockerCliContextResponse("test", true, WINDOWS_HOST));
|
||||||
|
DockerHost host = DockerHost.get(null, NO_SYSTEM_ENV, () -> context);
|
||||||
|
assertThat(host).hasToString("127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWhenHasWslContext() {
|
||||||
|
List<DockerCliContextResponse> context = List.of(new DockerCliContextResponse("test", true, WSL_HOST));
|
||||||
|
DockerHost host = DockerHost.get(null, NO_SYSTEM_ENV, () -> context);
|
||||||
|
assertThat(host).hasToString("127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWhenHasHttpContext() {
|
||||||
|
List<DockerCliContextResponse> context = List.of(new DockerCliContextResponse("test", true, HTTP_HOST));
|
||||||
|
DockerHost host = DockerHost.get(null, NO_SYSTEM_ENV, () -> context);
|
||||||
|
assertThat(host).hasToString("192.168.1.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWhenHasHttpsContext() {
|
||||||
|
List<DockerCliContextResponse> context = List.of(new DockerCliContextResponse("test", true, HTTPS_HOST));
|
||||||
|
DockerHost host = DockerHost.get(null, NO_SYSTEM_ENV, () -> context);
|
||||||
|
assertThat(host).hasToString("192.168.1.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWhenHasTcpContext() {
|
||||||
|
List<DockerCliContextResponse> context = List.of(new DockerCliContextResponse("test", true, TCP_HOST));
|
||||||
|
DockerHost host = DockerHost.get(null, NO_SYSTEM_ENV, () -> context);
|
||||||
|
assertThat(host).hasToString("192.168.1.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWhenContextHasMultiple() {
|
||||||
|
List<DockerCliContextResponse> context = new ArrayList<>();
|
||||||
|
context.add(new DockerCliContextResponse("test", false, "http://192.168.1.1"));
|
||||||
|
context.add(new DockerCliContextResponse("test", true, "http://192.168.1.2"));
|
||||||
|
context.add(new DockerCliContextResponse("test", false, "http://192.168.1.3"));
|
||||||
|
DockerHost host = DockerHost.get(null, NO_SYSTEM_ENV, () -> context);
|
||||||
|
assertThat(host).hasToString("192.168.1.2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getWhenHasNone() {
|
||||||
|
DockerHost host = DockerHost.get(null, NO_SYSTEM_ENV, NO_CONTEXT);
|
||||||
|
assertThat(host).hasToString("127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DockerJson}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DockerJsonTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deserializeWhenSentenceCase() {
|
||||||
|
String json = """
|
||||||
|
{ "Value": 1 }
|
||||||
|
""";
|
||||||
|
TestResponse response = DockerJson.deserialize(json, TestResponse.class);
|
||||||
|
assertThat(response).isEqualTo(new TestResponse(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deserializeWhenLowerCase() {
|
||||||
|
String json = """
|
||||||
|
{ "value": 1 }
|
||||||
|
""";
|
||||||
|
TestResponse response = DockerJson.deserialize(json, TestResponse.class);
|
||||||
|
assertThat(response).isEqualTo(new TestResponse(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deserializeToListWhenArray() {
|
||||||
|
String json = """
|
||||||
|
[{ "value": 1 }, { "value": 2 }]
|
||||||
|
""";
|
||||||
|
List<TestResponse> response = DockerJson.deserializeToList(json, TestResponse.class);
|
||||||
|
assertThat(response).containsExactly(new TestResponse(1), new TestResponse(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deserializeToListWhenMultipleLines() {
|
||||||
|
String json = """
|
||||||
|
{ "Value": 1 }
|
||||||
|
{ "Value": 2 }
|
||||||
|
""";
|
||||||
|
List<TestResponse> response = DockerJson.deserializeToList(json, TestResponse.class);
|
||||||
|
assertThat(response).containsExactly(new TestResponse(1), new TestResponse(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
record TestResponse(int value) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link ImageReference}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class ImageReferenceTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getImageNameWhenImageOnly() {
|
||||||
|
ImageReference imageReference = ImageReference.of("redis");
|
||||||
|
assertThat(imageReference.getImageName()).isEqualTo("redis");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getImageNameWhenImageAndTag() {
|
||||||
|
ImageReference imageReference = ImageReference.of("redis:5");
|
||||||
|
assertThat(imageReference.getImageName()).isEqualTo("redis");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getImageNameWhenImageAndDigest() {
|
||||||
|
ImageReference imageReference = ImageReference
|
||||||
|
.of("redis@sha256:0ed5d5928d4737458944eb604cc8509e245c3e19d02ad83935398bc4b991aac7");
|
||||||
|
assertThat(imageReference.getImageName()).isEqualTo("redis");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getImageNameWhenProjectAndImage() {
|
||||||
|
ImageReference imageReference = ImageReference.of("library/redis");
|
||||||
|
assertThat(imageReference.getImageName()).isEqualTo("redis");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getImageNameWhenRegistryLibraryAndImage() {
|
||||||
|
ImageReference imageReference = ImageReference.of("docker.io/library/redis");
|
||||||
|
assertThat(imageReference.getImageName()).isEqualTo("redis");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getImageNameWhenRegistryLibraryImageAndTag() {
|
||||||
|
ImageReference imageReference = ImageReference.of("docker.io/library/redis:5");
|
||||||
|
assertThat(imageReference.getImageName()).isEqualTo("redis");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getImageNameWhenRegistryLibraryImageAndDigest() {
|
||||||
|
ImageReference imageReference = ImageReference
|
||||||
|
.of("docker.io/library/redis@sha256:0ed5d5928d4737458944eb604cc8509e245c3e19d02ad83935398bc4b991aac7");
|
||||||
|
assertThat(imageReference.getImageName()).isEqualTo("redis");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getImageNameWhenRegistryWithPort() {
|
||||||
|
ImageReference imageReference = ImageReference.of("my_private.registry:5000/redis");
|
||||||
|
assertThat(imageReference.getImageName()).isEqualTo("redis");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getImageNameWhenRegistryWithPortAndTag() {
|
||||||
|
ImageReference imageReference = ImageReference.of("my_private.registry:5000/redis:5");
|
||||||
|
assertThat(imageReference.getImageName()).isEqualTo("redis");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void toStringReturnsReferenceString() {
|
||||||
|
ImageReference imageReference = ImageReference.of("docker.io/library/redis");
|
||||||
|
assertThat(imageReference).hasToString("docker.io/library/redis");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void equalsAndHashCode() {
|
||||||
|
ImageReference imageReference1 = ImageReference.of("docker.io/library/redis");
|
||||||
|
ImageReference imageReference2 = ImageReference.of("docker.io/library/redis");
|
||||||
|
ImageReference imageReference3 = ImageReference.of("docker.io/library/other");
|
||||||
|
assertThat(imageReference1.hashCode()).isEqualTo(imageReference2.hashCode());
|
||||||
|
assertThat(imageReference1).isEqualTo(imageReference1).isEqualTo(imageReference2).isNotEqualTo(imageReference3);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.core;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.testsupport.process.DisabledIfProcessUnavailable;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link ProcessRunner}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
@DisabledIfProcessUnavailable("docker")
|
||||||
|
class ProcessRunnerTests {
|
||||||
|
|
||||||
|
private ProcessRunner processRunner = new ProcessRunner();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void run() {
|
||||||
|
String out = this.processRunner.run("docker", "--version");
|
||||||
|
assertThat(out).isNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void runWhenProcessDoesNotStart() {
|
||||||
|
assertThatExceptionOfType(ProcessStartException.class)
|
||||||
|
.isThrownBy(() -> this.processRunner.run("iverymuchdontexist", "--version"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void runWhenProcessReturnsNonZeroExitCode() {
|
||||||
|
assertThatExceptionOfType(ProcessExitException.class)
|
||||||
|
.isThrownBy(() -> this.processRunner.run("docker", "-thisdoesntwork"))
|
||||||
|
.satisfies((ex) -> {
|
||||||
|
assertThat(ex.getExitCode()).isGreaterThan(0);
|
||||||
|
assertThat(ex.getStdOut()).isEmpty();
|
||||||
|
assertThat(ex.getStdErr()).isNotEmpty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,373 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.lifecycle;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplicationShutdownHandlers;
|
||||||
|
import org.springframework.boot.context.properties.bind.Binder;
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerCompose;
|
||||||
|
import org.springframework.boot.docker.compose.core.DockerComposeFile;
|
||||||
|
import org.springframework.boot.docker.compose.core.RunningService;
|
||||||
|
import org.springframework.boot.docker.compose.readiness.ServiceReadinessChecks;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationListener;
|
||||||
|
import org.springframework.context.support.GenericApplicationContext;
|
||||||
|
import org.springframework.util.FileCopyUtils;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.isA;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.BDDMockito.then;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DockerComposeLifecycleManager}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DockerComposeLifecycleManagerTests {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
File temp;
|
||||||
|
|
||||||
|
private DockerComposeFile dockerComposeFile;
|
||||||
|
|
||||||
|
private DockerCompose dockerCompose;
|
||||||
|
|
||||||
|
private Set<String> activeProfiles;
|
||||||
|
|
||||||
|
private GenericApplicationContext applicationContext;
|
||||||
|
|
||||||
|
private TestSpringApplicationShutdownHandlers shutdownHandlers;
|
||||||
|
|
||||||
|
private ServiceReadinessChecks serviceReadinessChecks;
|
||||||
|
|
||||||
|
private List<RunningService> runningServices;
|
||||||
|
|
||||||
|
private DockerComposeProperties properties;
|
||||||
|
|
||||||
|
private LinkedHashSet<ApplicationListener<?>> eventListeners;
|
||||||
|
|
||||||
|
private DockerComposeLifecycleManager lifecycleManager;
|
||||||
|
|
||||||
|
private DockerComposeSkipCheck skipCheck;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() throws IOException {
|
||||||
|
File file = new File(this.temp, "compose.yml");
|
||||||
|
FileCopyUtils.copy(new byte[0], file);
|
||||||
|
this.dockerComposeFile = DockerComposeFile.of(file);
|
||||||
|
this.dockerCompose = mock(DockerCompose.class);
|
||||||
|
File workingDirectory = new File(".");
|
||||||
|
this.applicationContext = new GenericApplicationContext();
|
||||||
|
this.applicationContext.refresh();
|
||||||
|
Binder binder = Binder.get(this.applicationContext.getEnvironment());
|
||||||
|
this.shutdownHandlers = new TestSpringApplicationShutdownHandlers();
|
||||||
|
this.properties = DockerComposeProperties.get(binder);
|
||||||
|
this.eventListeners = new LinkedHashSet<>();
|
||||||
|
this.skipCheck = mock(DockerComposeSkipCheck.class);
|
||||||
|
this.serviceReadinessChecks = mock(ServiceReadinessChecks.class);
|
||||||
|
this.lifecycleManager = new TestDockerComposeLifecycleManager(workingDirectory, this.applicationContext, binder,
|
||||||
|
this.shutdownHandlers, this.properties, this.eventListeners, this.skipCheck,
|
||||||
|
this.serviceReadinessChecks);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startupWhenEnabledFalseDoesNotStart() {
|
||||||
|
this.properties.setEnabled(false);
|
||||||
|
EventCapturingListener listener = new EventCapturingListener();
|
||||||
|
this.eventListeners.add(listener);
|
||||||
|
setupRunningServices();
|
||||||
|
this.lifecycleManager.startup();
|
||||||
|
assertThat(listener.getEvent()).isNull();
|
||||||
|
then(this.dockerCompose).should(never()).hasDefinedServices();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startupWhenInTestDoesNotStart() {
|
||||||
|
given(this.skipCheck.shouldSkip(any(), any(), any())).willReturn(true);
|
||||||
|
EventCapturingListener listener = new EventCapturingListener();
|
||||||
|
this.eventListeners.add(listener);
|
||||||
|
setupRunningServices();
|
||||||
|
this.lifecycleManager.startup();
|
||||||
|
assertThat(listener.getEvent()).isNull();
|
||||||
|
then(this.dockerCompose).should(never()).hasDefinedServices();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startupWhenHasNoDefinedServicesDoesNothing() {
|
||||||
|
EventCapturingListener listener = new EventCapturingListener();
|
||||||
|
this.eventListeners.add(listener);
|
||||||
|
this.lifecycleManager.startup();
|
||||||
|
assertThat(listener.getEvent()).isNull();
|
||||||
|
then(this.dockerCompose).should().hasDefinedServices();
|
||||||
|
then(this.dockerCompose).should(never()).up();
|
||||||
|
then(this.dockerCompose).should(never()).start();
|
||||||
|
then(this.dockerCompose).should(never()).down(isA(Duration.class));
|
||||||
|
then(this.dockerCompose).should(never()).stop(isA(Duration.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startupWhenLifecycleStartAndStopAndHasNoRunningServicesDoesStartupAndShutdown() {
|
||||||
|
this.properties.setLifecycleManagement(LifecycleManagement.START_AND_STOP);
|
||||||
|
EventCapturingListener listener = new EventCapturingListener();
|
||||||
|
this.eventListeners.add(listener);
|
||||||
|
given(this.dockerCompose.hasDefinedServices()).willReturn(true);
|
||||||
|
this.lifecycleManager.startup();
|
||||||
|
this.shutdownHandlers.run();
|
||||||
|
assertThat(listener.getEvent()).isNotNull();
|
||||||
|
then(this.dockerCompose).should().up();
|
||||||
|
then(this.dockerCompose).should(never()).start();
|
||||||
|
then(this.dockerCompose).should().down(isA(Duration.class));
|
||||||
|
then(this.dockerCompose).should(never()).stop(isA(Duration.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startupWhenLifecycleStartAndStopAndHasRunningServicesDoesNoStartupOrShutdown() {
|
||||||
|
this.properties.setLifecycleManagement(LifecycleManagement.START_AND_STOP);
|
||||||
|
EventCapturingListener listener = new EventCapturingListener();
|
||||||
|
this.eventListeners.add(listener);
|
||||||
|
setupRunningServices();
|
||||||
|
this.lifecycleManager.startup();
|
||||||
|
this.shutdownHandlers.run();
|
||||||
|
assertThat(listener.getEvent()).isNotNull();
|
||||||
|
then(this.dockerCompose).should(never()).up();
|
||||||
|
then(this.dockerCompose).should(never()).start();
|
||||||
|
then(this.dockerCompose).should(never()).down(isA(Duration.class));
|
||||||
|
then(this.dockerCompose).should(never()).stop(isA(Duration.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startupWhenLifecycleNoneDoesNoStartupOrShutdown() {
|
||||||
|
this.properties.setLifecycleManagement(LifecycleManagement.NONE);
|
||||||
|
EventCapturingListener listener = new EventCapturingListener();
|
||||||
|
this.eventListeners.add(listener);
|
||||||
|
setupRunningServices();
|
||||||
|
this.lifecycleManager.startup();
|
||||||
|
this.shutdownHandlers.run();
|
||||||
|
assertThat(listener.getEvent()).isNotNull();
|
||||||
|
then(this.dockerCompose).should(never()).up();
|
||||||
|
then(this.dockerCompose).should(never()).start();
|
||||||
|
then(this.dockerCompose).should(never()).down(isA(Duration.class));
|
||||||
|
then(this.dockerCompose).should(never()).stop(isA(Duration.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startupWhenLifecycleStartOnlyDoesStartupAndNoShutdown() {
|
||||||
|
this.properties.setLifecycleManagement(LifecycleManagement.START_ONLY);
|
||||||
|
EventCapturingListener listener = new EventCapturingListener();
|
||||||
|
this.eventListeners.add(listener);
|
||||||
|
given(this.dockerCompose.hasDefinedServices()).willReturn(true);
|
||||||
|
this.lifecycleManager.startup();
|
||||||
|
this.shutdownHandlers.run();
|
||||||
|
assertThat(listener.getEvent()).isNotNull();
|
||||||
|
then(this.dockerCompose).should().up();
|
||||||
|
then(this.dockerCompose).should(never()).start();
|
||||||
|
then(this.dockerCompose).should(never()).down(isA(Duration.class));
|
||||||
|
then(this.dockerCompose).should(never()).stop(isA(Duration.class));
|
||||||
|
this.shutdownHandlers.assertNoneAdded();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startupWhenStartupCommandStartDoesStartupUsingStartAndShutdown() {
|
||||||
|
this.properties.setLifecycleManagement(LifecycleManagement.START_AND_STOP);
|
||||||
|
this.properties.getStartup().setCommand(StartupCommand.START);
|
||||||
|
EventCapturingListener listener = new EventCapturingListener();
|
||||||
|
this.eventListeners.add(listener);
|
||||||
|
given(this.dockerCompose.hasDefinedServices()).willReturn(true);
|
||||||
|
this.lifecycleManager.startup();
|
||||||
|
this.shutdownHandlers.run();
|
||||||
|
assertThat(listener.getEvent()).isNotNull();
|
||||||
|
then(this.dockerCompose).should(never()).up();
|
||||||
|
then(this.dockerCompose).should().start();
|
||||||
|
then(this.dockerCompose).should().down(isA(Duration.class));
|
||||||
|
then(this.dockerCompose).should(never()).stop(isA(Duration.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startupWhenShutdownCommandStopDoesStartupAndShutdownUsingStop() {
|
||||||
|
this.properties.setLifecycleManagement(LifecycleManagement.START_AND_STOP);
|
||||||
|
this.properties.getShutdown().setCommand(ShutdownCommand.STOP);
|
||||||
|
EventCapturingListener listener = new EventCapturingListener();
|
||||||
|
this.eventListeners.add(listener);
|
||||||
|
given(this.dockerCompose.hasDefinedServices()).willReturn(true);
|
||||||
|
this.lifecycleManager.startup();
|
||||||
|
this.shutdownHandlers.run();
|
||||||
|
assertThat(listener.getEvent()).isNotNull();
|
||||||
|
then(this.dockerCompose).should().up();
|
||||||
|
then(this.dockerCompose).should(never()).start();
|
||||||
|
then(this.dockerCompose).should(never()).down(isA(Duration.class));
|
||||||
|
then(this.dockerCompose).should().stop(isA(Duration.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startupWhenHasShutdownTimeoutUsesDuration() {
|
||||||
|
this.properties.setLifecycleManagement(LifecycleManagement.START_AND_STOP);
|
||||||
|
Duration timeout = Duration.ofDays(1);
|
||||||
|
this.properties.getShutdown().setTimeout(timeout);
|
||||||
|
EventCapturingListener listener = new EventCapturingListener();
|
||||||
|
this.eventListeners.add(listener);
|
||||||
|
given(this.dockerCompose.hasDefinedServices()).willReturn(true);
|
||||||
|
this.lifecycleManager.startup();
|
||||||
|
this.shutdownHandlers.run();
|
||||||
|
assertThat(listener.getEvent()).isNotNull();
|
||||||
|
then(this.dockerCompose).should().down(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startupWhenHasIgnoreLabelIgnoresService() {
|
||||||
|
EventCapturingListener listener = new EventCapturingListener();
|
||||||
|
this.eventListeners.add(listener);
|
||||||
|
setupRunningServices(Map.of("org.springframework.boot.ignore", "true"));
|
||||||
|
this.lifecycleManager.startup();
|
||||||
|
this.shutdownHandlers.run();
|
||||||
|
assertThat(listener.getEvent()).isNotNull();
|
||||||
|
assertThat(listener.getEvent().getRunningServices()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startupWaitsUntilReady() {
|
||||||
|
EventCapturingListener listener = new EventCapturingListener();
|
||||||
|
this.eventListeners.add(listener);
|
||||||
|
setupRunningServices();
|
||||||
|
this.lifecycleManager.startup();
|
||||||
|
this.shutdownHandlers.run();
|
||||||
|
then(this.serviceReadinessChecks).should().waitUntilReady(this.runningServices);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startupGetsDockerComposeWithActiveProfiles() {
|
||||||
|
this.properties.getProfiles().setActive(Set.of("my-profile"));
|
||||||
|
setupRunningServices();
|
||||||
|
this.lifecycleManager.startup();
|
||||||
|
assertThat(this.activeProfiles).containsExactly("my-profile");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startupPublishesEvent() {
|
||||||
|
EventCapturingListener listener = new EventCapturingListener();
|
||||||
|
this.eventListeners.add(listener);
|
||||||
|
setupRunningServices();
|
||||||
|
this.lifecycleManager.startup();
|
||||||
|
DockerComposeServicesReadyEvent event = listener.getEvent();
|
||||||
|
assertThat(event).isNotNull();
|
||||||
|
assertThat(event.getSource()).isEqualTo(this.applicationContext);
|
||||||
|
assertThat(event.getRunningServices()).isEqualTo(this.runningServices);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupRunningServices() {
|
||||||
|
setupRunningServices(Collections.emptyMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupRunningServices(Map<String, String> labels) {
|
||||||
|
given(this.dockerCompose.hasDefinedServices()).willReturn(true);
|
||||||
|
given(this.dockerCompose.hasRunningServices()).willReturn(true);
|
||||||
|
RunningService runningService = mock(RunningService.class);
|
||||||
|
given(runningService.labels()).willReturn(labels);
|
||||||
|
this.runningServices = List.of(runningService);
|
||||||
|
given(this.dockerCompose.getRunningServices()).willReturn(this.runningServices);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testable {@link SpringApplicationShutdownHandlers}.
|
||||||
|
*/
|
||||||
|
static class TestSpringApplicationShutdownHandlers implements SpringApplicationShutdownHandlers {
|
||||||
|
|
||||||
|
private final List<Runnable> actions = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void add(Runnable action) {
|
||||||
|
this.actions.add(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(Runnable action) {
|
||||||
|
this.actions.remove(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
void run() {
|
||||||
|
this.actions.forEach(Runnable::run);
|
||||||
|
}
|
||||||
|
|
||||||
|
void assertNoneAdded() {
|
||||||
|
assertThat(this.actions).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ApplicationListener} to capture the {@link DockerComposeServicesReadyEvent}.
|
||||||
|
*/
|
||||||
|
static class EventCapturingListener implements ApplicationListener<DockerComposeServicesReadyEvent> {
|
||||||
|
|
||||||
|
private DockerComposeServicesReadyEvent event;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplicationEvent(DockerComposeServicesReadyEvent event) {
|
||||||
|
this.event = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
DockerComposeServicesReadyEvent getEvent() {
|
||||||
|
return this.event;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testable {@link DockerComposeLifecycleManager}.
|
||||||
|
*/
|
||||||
|
class TestDockerComposeLifecycleManager extends DockerComposeLifecycleManager {
|
||||||
|
|
||||||
|
TestDockerComposeLifecycleManager(File workingDirectory, ApplicationContext applicationContext, Binder binder,
|
||||||
|
SpringApplicationShutdownHandlers shutdownHandlers, DockerComposeProperties properties,
|
||||||
|
Set<ApplicationListener<?>> eventListeners, DockerComposeSkipCheck skipCheck,
|
||||||
|
ServiceReadinessChecks serviceReadinessChecks) {
|
||||||
|
super(workingDirectory, applicationContext, binder, shutdownHandlers, properties, eventListeners, skipCheck,
|
||||||
|
serviceReadinessChecks);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected DockerComposeFile getComposeFile() {
|
||||||
|
return DockerComposeLifecycleManagerTests.this.dockerComposeFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected DockerCompose getDockerCompose(DockerComposeFile composeFile, Set<String> activeProfiles) {
|
||||||
|
DockerComposeLifecycleManagerTests.this.activeProfiles = activeProfiles;
|
||||||
|
return DockerComposeLifecycleManagerTests.this.dockerCompose;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.docker.compose.lifecycle;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.SpringApplicationShutdownHandlers;
|
||||||
|
import org.springframework.boot.context.event.ApplicationPreparedEvent;
|
||||||
|
import org.springframework.boot.context.properties.bind.Binder;
|
||||||
|
import org.springframework.context.ApplicationListener;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
import org.springframework.mock.env.MockEnvironment;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.BDDMockito.then;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link DockerComposeListener}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class DockerComposeListenerTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void onApplicationPreparedEventCreatesAndStartsDockerComposeLifecycleManager() {
|
||||||
|
SpringApplicationShutdownHandlers shutdownHandlers = mock(SpringApplicationShutdownHandlers.class);
|
||||||
|
SpringApplication application = mock(SpringApplication.class);
|
||||||
|
ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class);
|
||||||
|
MockEnvironment environment = new MockEnvironment();
|
||||||
|
given(context.getEnvironment()).willReturn(environment);
|
||||||
|
TestDockerComposeListener listener = new TestDockerComposeListener(shutdownHandlers, context);
|
||||||
|
ApplicationPreparedEvent event = new ApplicationPreparedEvent(application, new String[0], context);
|
||||||
|
listener.onApplicationEvent(event);
|
||||||
|
assertThat(listener.getManager()).isNotNull();
|
||||||
|
then(listener.getManager()).should().startup();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestDockerComposeListener extends DockerComposeListener {
|
||||||
|
|
||||||
|
private final ConfigurableApplicationContext context;
|
||||||
|
|
||||||
|
private DockerComposeLifecycleManager manager;
|
||||||
|
|
||||||
|
TestDockerComposeListener(SpringApplicationShutdownHandlers shutdownHandlers,
|
||||||
|
ConfigurableApplicationContext context) {
|
||||||
|
super(shutdownHandlers);
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected DockerComposeLifecycleManager createDockerComposeLifecycleManager(
|
||||||
|
ConfigurableApplicationContext applicationContext, Binder binder, DockerComposeProperties properties,
|
||||||
|
Set<ApplicationListener<?>> eventListeners) {
|
||||||
|
this.manager = mock(DockerComposeLifecycleManager.class);
|
||||||
|
assertThat(applicationContext).isSameAs(this.context);
|
||||||
|
assertThat(binder).isNotNull();
|
||||||
|
assertThat(properties).isNotNull();
|
||||||
|
return this.manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
DockerComposeLifecycleManager getManager() {
|
||||||
|
return this.manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue