Merge pull request #20936 from dreis2211
* pr/20936: Upgrade to Testcontainers 1.14.0 Closes gh-20936pull/20973/head
commit
c6a55a5455
@ -1,504 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.couchbase;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.couchbase.client.core.env.SeedNode;
|
||||
import com.couchbase.client.core.env.TimeoutConfig;
|
||||
import com.couchbase.client.core.util.UrlQueryStringBuilder;
|
||||
import com.couchbase.client.java.Bucket;
|
||||
import com.couchbase.client.java.Cluster;
|
||||
import com.couchbase.client.java.ClusterOptions;
|
||||
import com.couchbase.client.java.env.ClusterEnvironment;
|
||||
import com.couchbase.client.java.manager.bucket.BucketSettings;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.github.dockerjava.api.command.ExecCreateCmdResponse;
|
||||
import com.github.dockerjava.api.command.InspectContainerResponse;
|
||||
import com.github.dockerjava.core.command.ExecStartResultCallback;
|
||||
import org.apache.commons.compress.utils.Sets;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.Network;
|
||||
import org.testcontainers.containers.SocatContainer;
|
||||
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
|
||||
import org.testcontainers.images.builder.Transferable;
|
||||
import org.testcontainers.shaded.org.apache.commons.io.IOUtils;
|
||||
import org.testcontainers.utility.ThrowingFunction;
|
||||
|
||||
import static java.net.HttpURLConnection.HTTP_OK;
|
||||
import static org.testcontainers.shaded.org.apache.commons.codec.binary.Base64.encodeBase64String;
|
||||
|
||||
/**
|
||||
* Temporary copy of TestContainers's Couchbase support until it works against Couchbase
|
||||
* SDK v3.
|
||||
*/
|
||||
@SuppressWarnings("resource")
|
||||
class CouchbaseContainer extends GenericContainer<CouchbaseContainer> {
|
||||
|
||||
public static final String VERSION = "5.5.1";
|
||||
|
||||
public static final String DOCKER_IMAGE_NAME = "couchbase/server:";
|
||||
|
||||
public static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
public static final String STATIC_CONFIG = "/opt/couchbase/etc/couchbase/static_config";
|
||||
|
||||
public static final String CAPI_CONFIG = "/opt/couchbase/etc/couchdb/default.d/capi.ini";
|
||||
|
||||
private static final int REQUIRED_DEFAULT_PASSWORD_LENGTH = 6;
|
||||
|
||||
private String memoryQuota = "300";
|
||||
|
||||
private String indexMemoryQuota = "300";
|
||||
|
||||
private String clusterUsername = "Administrator";
|
||||
|
||||
private String clusterPassword = "password";
|
||||
|
||||
private boolean keyValue = true;
|
||||
|
||||
private boolean query = true;
|
||||
|
||||
private boolean index = true;
|
||||
|
||||
private boolean primaryIndex = false;
|
||||
|
||||
private boolean fts = false;
|
||||
|
||||
private ClusterEnvironment couchbaseEnvironment;
|
||||
|
||||
private Cluster couchbaseCluster;
|
||||
|
||||
private final List<BucketAndUserSettings> newBuckets = new ArrayList<>();
|
||||
|
||||
private String urlBase;
|
||||
|
||||
private SocatContainer proxy;
|
||||
|
||||
CouchbaseContainer() {
|
||||
this(DOCKER_IMAGE_NAME + VERSION);
|
||||
}
|
||||
|
||||
CouchbaseContainer(String imageName) {
|
||||
super(imageName);
|
||||
|
||||
withNetwork(Network.SHARED);
|
||||
setWaitStrategy(new HttpWaitStrategy().forPath("/ui/index.html"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Integer> getLivenessCheckPortNumbers() {
|
||||
return Sets.newHashSet(getMappedPort(CouchbasePort.REST));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
if (this.clusterPassword.length() < REQUIRED_DEFAULT_PASSWORD_LENGTH) {
|
||||
logger().warn("The provided cluster admin password length is less then the default password policy length. "
|
||||
+ "Cluster start will fail if configured password requirements are not met.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() {
|
||||
startProxy(getNetworkAliases().get(0));
|
||||
try {
|
||||
super.doStart();
|
||||
}
|
||||
catch (Throwable e) {
|
||||
this.proxy.stop();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void startProxy(String networkAlias) {
|
||||
this.proxy = new SocatContainer().withNetwork(getNetwork());
|
||||
|
||||
for (CouchbasePort port : CouchbaseContainer.CouchbasePort.values()) {
|
||||
if (port.isDynamic()) {
|
||||
this.proxy.withTarget(port.getOriginalPort(), networkAlias);
|
||||
}
|
||||
else {
|
||||
this.proxy.addExposedPort(port.getOriginalPort());
|
||||
}
|
||||
}
|
||||
|
||||
this.proxy.setWaitStrategy(null);
|
||||
this.proxy.start();
|
||||
|
||||
ExecCreateCmdResponse createCmdResponse = this.dockerClient.execCreateCmd(this.proxy.getContainerId())
|
||||
.withCmd("sh", "-c",
|
||||
Stream.of(CouchbaseContainer.CouchbasePort.values())
|
||||
.map(port -> "/usr/bin/socat " + "TCP-LISTEN:" + port.getOriginalPort()
|
||||
+ ",fork,reuseaddr " + "TCP:" + networkAlias + ":" + getMappedPort(port))
|
||||
.collect(Collectors.joining(" & ", "true", "")))
|
||||
.exec();
|
||||
try {
|
||||
this.dockerClient.execStartCmd(createCmdResponse.getId()).exec(new ExecStartResultCallback())
|
||||
.awaitCompletion(10, TimeUnit.SECONDS);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
throw new RuntimeException("Interrupted docker start", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Integer> getExposedPorts() {
|
||||
return this.proxy.getExposedPorts();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContainerIpAddress() {
|
||||
return this.proxy.getContainerIpAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getMappedPort(int originalPort) {
|
||||
return this.proxy.getMappedPort(originalPort);
|
||||
}
|
||||
|
||||
protected Integer getMappedPort(CouchbasePort port) {
|
||||
return getMappedPort(port.getOriginalPort());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Integer> getBoundPortNumbers() {
|
||||
return this.proxy.getBoundPortNumbers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
try {
|
||||
stopCluster();
|
||||
this.couchbaseCluster = null;
|
||||
this.couchbaseEnvironment = null;
|
||||
}
|
||||
finally {
|
||||
Stream.<Runnable>of(super::stop, this.proxy::stop).parallel().forEach(Runnable::run);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopCluster() {
|
||||
getCouchbaseCluster().disconnect();
|
||||
getCouchbaseEnvironment().shutdown();
|
||||
}
|
||||
|
||||
CouchbaseContainer withNewBucket(BucketSettings bucketSettings) {
|
||||
this.newBuckets.add(new BucketAndUserSettings(bucketSettings));
|
||||
return self();
|
||||
}
|
||||
|
||||
private void initUrlBase() {
|
||||
if (this.urlBase == null) {
|
||||
this.urlBase = String.format("http://%s:%s", getContainerIpAddress(), getMappedPort(CouchbasePort.REST));
|
||||
}
|
||||
}
|
||||
|
||||
void initCluster() throws Exception {
|
||||
String poolURL = "/pools/default";
|
||||
String poolPayload = "memoryQuota=" + URLEncoder.encode(this.memoryQuota, "UTF-8") + "&indexMemoryQuota="
|
||||
+ URLEncoder.encode(this.indexMemoryQuota, "UTF-8");
|
||||
|
||||
String setupServicesURL = "/node/controller/setupServices";
|
||||
StringBuilder servicePayloadBuilder = new StringBuilder();
|
||||
if (this.keyValue) {
|
||||
servicePayloadBuilder.append("kv,");
|
||||
}
|
||||
if (this.query) {
|
||||
servicePayloadBuilder.append("n1ql,");
|
||||
}
|
||||
if (this.index) {
|
||||
servicePayloadBuilder.append("index,");
|
||||
}
|
||||
if (this.fts) {
|
||||
servicePayloadBuilder.append("fts,");
|
||||
}
|
||||
String setupServiceContent = "services=" + URLEncoder.encode(servicePayloadBuilder.toString(), "UTF-8");
|
||||
|
||||
String webSettingsURL = "/settings/web";
|
||||
String webSettingsContent = "username=" + URLEncoder.encode(this.clusterUsername, "UTF-8") + "&password="
|
||||
+ URLEncoder.encode(this.clusterPassword, "UTF-8") + "&port=8091";
|
||||
|
||||
callCouchbaseRestAPI(poolURL, poolPayload);
|
||||
callCouchbaseRestAPI(setupServicesURL, setupServiceContent);
|
||||
callCouchbaseRestAPI(webSettingsURL, webSettingsContent);
|
||||
|
||||
createNodeWaitStrategy().waitUntilReady(this);
|
||||
callCouchbaseRestAPI("/settings/indexes",
|
||||
"indexerThreads=0&logLevel=info&maxRollbackPoints=5&storageMode=memory_optimized");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private HttpWaitStrategy createNodeWaitStrategy() {
|
||||
return new HttpWaitStrategy().forPath("/pools/default/")
|
||||
.withBasicCredentials(this.clusterUsername, this.clusterPassword).forStatusCode(HTTP_OK)
|
||||
.forResponsePredicate(response -> {
|
||||
try {
|
||||
return Optional.of(MAPPER.readTree(response)).map(n -> n.at("/nodes/0/status"))
|
||||
.map(JsonNode::asText).map("healthy"::equals).orElse(false);
|
||||
}
|
||||
catch (IOException e) {
|
||||
logger().error("Unable to parse response {}", response);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void createBucket(BucketSettings bucketSetting, boolean primaryIndex) throws IOException {
|
||||
// Insert Bucket
|
||||
String payload = convertSettingsToParams(bucketSetting, false).build();
|
||||
callCouchbaseRestAPI("/pools/default/buckets/", payload);
|
||||
|
||||
// Check that the bucket is ready before moving on
|
||||
new HttpWaitStrategy().forPath("/pools/default/buckets/" + bucketSetting.name())
|
||||
.withBasicCredentials(this.clusterUsername, this.clusterPassword).forStatusCode(HTTP_OK)
|
||||
.waitUntilReady(this);
|
||||
|
||||
if (this.index && this.primaryIndex) {
|
||||
Bucket bucket = getCouchbaseCluster().bucket(bucketSetting.name());
|
||||
bucket.waitUntilReady(Duration.ofSeconds(10));
|
||||
if (primaryIndex) {
|
||||
getCouchbaseCluster().queryIndexes().createPrimaryIndex(bucketSetting.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void callCouchbaseRestAPI(String url, String payload) throws IOException {
|
||||
initUrlBase();
|
||||
|
||||
String fullUrl = this.urlBase + url;
|
||||
HttpURLConnection httpConnection = (HttpURLConnection) ((new URL(fullUrl).openConnection()));
|
||||
httpConnection.setDoOutput(true);
|
||||
httpConnection.setRequestMethod("POST");
|
||||
httpConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
|
||||
String encoded = encodeBase64String(
|
||||
(this.clusterUsername + ":" + this.clusterPassword).getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
httpConnection.setRequestProperty("Authorization", "Basic " + encoded);
|
||||
DataOutputStream out = new DataOutputStream(httpConnection.getOutputStream());
|
||||
out.writeBytes(payload);
|
||||
out.flush();
|
||||
httpConnection.getResponseCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void containerIsCreated(String containerId) {
|
||||
patchConfig(STATIC_CONFIG, this::addMappedPorts);
|
||||
// capi needs a special configuration, see
|
||||
// https://developer.couchbase.com/documentation/server/current/install/install-ports.html
|
||||
patchConfig(CAPI_CONFIG, this::replaceCapiPort);
|
||||
}
|
||||
|
||||
private void patchConfig(String configLocation, ThrowingFunction<String, String> patchFunction) {
|
||||
String patchedConfig = copyFileFromContainer(configLocation,
|
||||
inputStream -> patchFunction.apply(IOUtils.toString(inputStream, StandardCharsets.UTF_8)));
|
||||
copyFileToContainer(Transferable.of(patchedConfig.getBytes(StandardCharsets.UTF_8)), configLocation);
|
||||
}
|
||||
|
||||
private String addMappedPorts(String originalConfig) {
|
||||
String portConfig = Stream.of(CouchbaseContainer.CouchbasePort.values()).filter(port -> !port.isDynamic())
|
||||
.map(port -> String.format("{%s, %d}.", port.name, getMappedPort(port)))
|
||||
.collect(Collectors.joining("\n"));
|
||||
return String.format("%s\n%s", originalConfig, portConfig);
|
||||
}
|
||||
|
||||
private String replaceCapiPort(String originalConfig) {
|
||||
return Arrays.stream(originalConfig.split("\n"))
|
||||
.map(s -> (s.matches("port\\s*=\\s*" + CouchbaseContainer.CouchbasePort.CAPI.getOriginalPort()))
|
||||
? "port = " + getMappedPort(CouchbaseContainer.CouchbasePort.CAPI) : s)
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void containerIsStarted(InspectContainerResponse containerInfo) {
|
||||
try {
|
||||
initCluster();
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException("Could not init cluster", e);
|
||||
}
|
||||
|
||||
if (!this.newBuckets.isEmpty()) {
|
||||
for (BucketAndUserSettings bucket : this.newBuckets) {
|
||||
try {
|
||||
createBucket(bucket.getBucketSettings(), this.primaryIndex);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException("Could not create bucket", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Cluster createCouchbaseCluster() {
|
||||
SeedNode seedNode = SeedNode.create(getContainerIpAddress(),
|
||||
Optional.of(getMappedPort(CouchbaseContainer.CouchbasePort.MEMCACHED)),
|
||||
Optional.of(getMappedPort(CouchbaseContainer.CouchbasePort.REST)));
|
||||
|
||||
return Cluster.connect(new HashSet<>(Collections.singletonList(seedNode)), ClusterOptions
|
||||
.clusterOptions(this.clusterUsername, this.clusterPassword).environment(getCouchbaseEnvironment()));
|
||||
}
|
||||
|
||||
synchronized ClusterEnvironment getCouchbaseEnvironment() {
|
||||
if (this.couchbaseEnvironment == null) {
|
||||
this.couchbaseEnvironment = createCouchbaseEnvironment();
|
||||
}
|
||||
return this.couchbaseEnvironment;
|
||||
}
|
||||
|
||||
synchronized Cluster getCouchbaseCluster() {
|
||||
if (this.couchbaseCluster == null) {
|
||||
this.couchbaseCluster = createCouchbaseCluster();
|
||||
}
|
||||
return this.couchbaseCluster;
|
||||
}
|
||||
|
||||
private ClusterEnvironment createCouchbaseEnvironment() {
|
||||
return ClusterEnvironment.builder().timeoutConfig(TimeoutConfig.kvTimeout(Duration.ofSeconds(10))).build();
|
||||
}
|
||||
|
||||
CouchbaseContainer withMemoryQuota(String memoryQuota) {
|
||||
this.memoryQuota = memoryQuota;
|
||||
return self();
|
||||
}
|
||||
|
||||
CouchbaseContainer withIndexMemoryQuota(String indexMemoryQuota) {
|
||||
this.indexMemoryQuota = indexMemoryQuota;
|
||||
return self();
|
||||
}
|
||||
|
||||
CouchbaseContainer withClusterAdmin(String username, String password) {
|
||||
this.clusterUsername = username;
|
||||
this.clusterPassword = password;
|
||||
return self();
|
||||
}
|
||||
|
||||
CouchbaseContainer withKeyValue(boolean keyValue) {
|
||||
this.keyValue = keyValue;
|
||||
return self();
|
||||
}
|
||||
|
||||
CouchbaseContainer withQuery(boolean query) {
|
||||
this.query = query;
|
||||
return self();
|
||||
}
|
||||
|
||||
CouchbaseContainer withIndex(boolean index) {
|
||||
this.index = index;
|
||||
return self();
|
||||
}
|
||||
|
||||
CouchbaseContainer withPrimaryIndex(boolean primaryIndex) {
|
||||
this.primaryIndex = primaryIndex;
|
||||
return self();
|
||||
}
|
||||
|
||||
public CouchbaseContainer withFts(boolean fts) {
|
||||
this.fts = fts;
|
||||
return self();
|
||||
}
|
||||
|
||||
enum CouchbasePort {
|
||||
|
||||
REST("rest_port", 8091, true), CAPI("capi_port", 8092, false), QUERY("query_port", 8093, false), FTS(
|
||||
"fts_http_port", 8094, false), CBAS("cbas_http_port", 8095, false), EVENTING("eventing_http_port", 8096,
|
||||
false), MEMCACHED_SSL("memcached_ssl_port", 11207, false), MEMCACHED("memcached_port", 11210,
|
||||
false), REST_SSL("ssl_rest_port", 18091, true), CAPI_SSL("ssl_capi_port", 18092,
|
||||
false), QUERY_SSL("ssl_query_port", 18093, false), FTS_SSL("fts_ssl_port",
|
||||
18094, false), CBAS_SSL("cbas_ssl_port", 18095,
|
||||
false), EVENTING_SSL("eventing_ssl_port", 18096, false);
|
||||
|
||||
final String name;
|
||||
|
||||
final int originalPort;
|
||||
|
||||
final boolean dynamic;
|
||||
|
||||
CouchbasePort(String name, int originalPort, boolean dynamic) {
|
||||
this.name = name;
|
||||
this.originalPort = originalPort;
|
||||
this.dynamic = dynamic;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public int getOriginalPort() {
|
||||
return this.originalPort;
|
||||
}
|
||||
|
||||
public boolean isDynamic() {
|
||||
return this.dynamic;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class BucketAndUserSettings {
|
||||
|
||||
private final BucketSettings bucketSettings;
|
||||
|
||||
BucketAndUserSettings(BucketSettings bucketSettings) {
|
||||
this.bucketSettings = bucketSettings;
|
||||
}
|
||||
|
||||
BucketSettings getBucketSettings() {
|
||||
return this.bucketSettings;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private UrlQueryStringBuilder convertSettingsToParams(final BucketSettings settings, boolean update) {
|
||||
UrlQueryStringBuilder params = UrlQueryStringBuilder.createForUrlSafeNames();
|
||||
|
||||
params.add("ramQuotaMB", settings.ramQuotaMB());
|
||||
params.add("replicaNumber", settings.numReplicas());
|
||||
params.add("flushEnabled", settings.flushEnabled() ? 1 : 0);
|
||||
params.add("maxTTL", settings.maxTTL());
|
||||
params.add("evictionPolicy", settings.ejectionPolicy().alias());
|
||||
params.add("compressionMode", settings.compressionMode().alias());
|
||||
|
||||
// The following values must not be changed on update
|
||||
if (!update) {
|
||||
params.add("name", settings.name());
|
||||
params.add("bucketType", settings.bucketType().alias());
|
||||
params.add("conflictResolutionType", settings.conflictResolutionType().alias());
|
||||
params.add("replicaIndex", settings.replicaIndexes() ? 1 : 0);
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue