diff --git a/docs/modules/databases/couchbase.md b/docs/modules/databases/couchbase.md
index b71ee5de327..f6d014a42d4 100644
--- a/docs/modules/databases/couchbase.md
+++ b/docs/modules/databases/couchbase.md
@@ -8,25 +8,65 @@ Testcontainers module for Couchbase. [Couchbase](https://www.couchbase.com/) is
Running Couchbase as a stand-in in a test:
-1. Define a bucket:
-
- [Bucket Definition](../../../modules/couchbase/src/test/java/org/testcontainers/couchbase/CouchbaseContainerTest.java) inside_block:bucket_definition
-
-
-2. define a container:
-
- [Container definition](../../../modules/couchbase/src/test/java/org/testcontainers/couchbase/CouchbaseContainerTest.java) inside_block:container_definition
-
-
-3. create an environment & cluster:
-
- [Cluster creation](../../../modules/couchbase/src/test/java/org/testcontainers/couchbase/CouchbaseContainerTest.java) inside_block:cluster_creation
-
-
-4. authenticate:
-
- [Authentication](../../../modules/couchbase/src/test/java/org/testcontainers/couchbase/CouchbaseContainerTest.java) inside_block:auth
-
+### Create your own bucket
+
+```java
+public class SomeTest {
+
+ @Rule
+ public CouchbaseContainer couchbase = new CouchbaseContainer()
+ .withClusterAdmin("admin", "secret")
+ .withNewBucket(DefaultBucketSettings.builder()
+ .enableFlush(true)
+ .name("bucket-name")
+ .password("secret")
+ .quota(100)
+ .type(BucketType.COUCHBASE)
+ .build());
+
+ @Test
+ public void someTestMethod() {
+ Bucket bucket = couchbase.getCouchbaseCluster().openBucket("bucket-name");
+
+ // ... interact with client as if using Couchbase normally
+ }
+}
+```
+
+### Use preconfigured default bucket
+
+Bucket is cleared after each test
+
+```java
+public class SomeTest extends AbstractCouchbaseTest {
+
+ @Test
+ public void someTestMethod() {
+ Bucket bucket = getBucket();
+
+ // ... interact with client as if using Couchbase normally
+ }
+}
+```
+
+### Special consideration
+
+Couchbase container is configured to use random available [ports](https://developer.couchbase.com/documentation/server/current/install/install-ports.html) for some ports only, as [Couchbase Java SDK](https://developer.couchbase.com/documentation/server/current/sdk/java/start-using-sdk.html) permit to configure only some ports:
+
+- **8091** : REST/HTTP traffic ([bootstrapHttpDirectPort](http://docs.couchbase.com/sdk-api/couchbase-java-client-2.4.6/com/couchbase/client/java/env/DefaultCouchbaseEnvironment.Builder.html#bootstrapCarrierDirectPort-int-))
+- **18091** : REST/HTTP traffic with SSL ([bootstrapHttpSslPort](http://docs.couchbase.com/sdk-api/couchbase-java-client-2.4.6/com/couchbase/client/java/env/DefaultCouchbaseEnvironment.Builder.html#bootstrapCarrierSslPort-int-))
+- **11210** : memcached ([bootstrapCarrierDirectPort](http://docs.couchbase.com/sdk-api/couchbase-java-client-2.4.6/com/couchbase/client/java/env/DefaultCouchbaseEnvironment.Builder.html#bootstrapCarrierDirectPort-int-))
+- **11207** : memcached SSL ([bootstrapCarrierSslPort](http://docs.couchbase.com/sdk-api/couchbase-java-client-2.4.6/com/couchbase/client/java/env/DefaultCouchbaseEnvironment.Builder.html#bootstrapCarrierSslPort-int-))
+
+All other ports cannot be changed by Java SDK, there are sadly fixed:
+
+- **8092** : Queries, views, XDCR
+- **8093** : REST/HTTP Query service
+- **8094** : REST/HTTP Search Service
+- **8095** : REST/HTTP Analytic service
+
+So if you disable Query, Search and Analytic service, you can run multiple instance of this container, otherwise, you're stuck with one instance, for now.
+
## Adding this module to your project dependencies
diff --git a/modules/couchbase/build.gradle b/modules/couchbase/build.gradle
index 095f8828aac..cf4acd4e1c3 100644
--- a/modules/couchbase/build.gradle
+++ b/modules/couchbase/build.gradle
@@ -2,6 +2,8 @@ description = "Testcontainers :: Couchbase"
dependencies {
compile project(':testcontainers')
+ compile 'com.couchbase.client:java-client:2.7.13'
+ compileOnly 'org.jetbrains:annotations:20.1.0'
- testCompile 'com.couchbase.client:java-client:2.7.13'
+ testCompile project(':test-support')
}
diff --git a/modules/couchbase/src/main/java/org/testcontainers/couchbase/AbstractCouchbaseTest.java b/modules/couchbase/src/main/java/org/testcontainers/couchbase/AbstractCouchbaseTest.java
new file mode 100644
index 00000000000..7fd99ca9fbc
--- /dev/null
+++ b/modules/couchbase/src/main/java/org/testcontainers/couchbase/AbstractCouchbaseTest.java
@@ -0,0 +1,72 @@
+package org.testcontainers.couchbase;
+
+import com.couchbase.client.java.Bucket;
+import com.couchbase.client.java.CouchbaseCluster;
+import com.couchbase.client.java.bucket.BucketType;
+import com.couchbase.client.java.cluster.BucketSettings;
+import com.couchbase.client.java.cluster.DefaultBucketSettings;
+import com.couchbase.client.java.query.N1qlParams;
+import com.couchbase.client.java.query.N1qlQuery;
+import com.couchbase.client.java.query.consistency.ScanConsistency;
+import org.junit.After;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Basic class that can be used for couchbase tests. It will clear the database after every test.
+ */
+public abstract class AbstractCouchbaseTest {
+
+ public static final String TEST_BUCKET = "test";
+
+ public static final String DEFAULT_PASSWORD = "password";
+
+ Bucket bucket;
+
+ @After
+ public void clear() {
+ try {
+ if (getCouchbaseContainer().isIndex() && getCouchbaseContainer().isQuery() && getCouchbaseContainer().isPrimaryIndex()) {
+ getBucket().query(
+ N1qlQuery.simple(String.format("DELETE FROM `%s`", getBucket().name()),
+ N1qlParams.build().consistency(ScanConsistency.STATEMENT_PLUS)));
+ } else {
+ getBucket().bucketManager().flush();
+ }
+ } finally {
+ bucket.close(60, TimeUnit.SECONDS);
+ bucket = null;
+ }
+ }
+
+ protected abstract CouchbaseContainer getCouchbaseContainer();
+
+ protected static CouchbaseContainer initCouchbaseContainer(String imageName) {
+ CouchbaseContainer couchbaseContainer = (imageName == null) ? new CouchbaseContainer() : new CouchbaseContainer(imageName);
+ couchbaseContainer.withNewBucket(getDefaultBucketSettings());
+ return couchbaseContainer;
+ }
+
+ protected static BucketSettings getDefaultBucketSettings() {
+ return DefaultBucketSettings.builder()
+ .enableFlush(true)
+ .name(TEST_BUCKET)
+ .password(DEFAULT_PASSWORD)
+ .quota(100)
+ .replicas(0)
+ .type(BucketType.COUCHBASE)
+ .build();
+ }
+
+ protected synchronized Bucket getBucket() {
+ if (bucket == null) {
+ bucket = openBucket(TEST_BUCKET, DEFAULT_PASSWORD);
+ }
+ return bucket;
+ }
+
+ private Bucket openBucket(String bucketName, String password) {
+ CouchbaseCluster cluster = getCouchbaseContainer().getCouchbaseCluster();
+ return cluster.openBucket(bucketName, password);
+ }
+}
diff --git a/modules/couchbase/src/main/java/org/testcontainers/couchbase/BucketDefinition.java b/modules/couchbase/src/main/java/org/testcontainers/couchbase/BucketDefinition.java
deleted file mode 100644
index 8c2b11403ad..00000000000
--- a/modules/couchbase/src/main/java/org/testcontainers/couchbase/BucketDefinition.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (c) 2020 Couchbase, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.testcontainers.couchbase;
-
-/**
- * Allows to configure the properties of a bucket that should be created.
- */
-public class BucketDefinition {
-
- private final String name;
- private boolean queryPrimaryIndex = true;
- private int quota = 100;
-
- public BucketDefinition(final String name) {
- this.name = name;
- }
-
- /**
- * Sets a custom bucket quota (100MB by default).
- *
- * @param quota the quota to set for the bucket.
- * @return this {@link BucketDefinition} for chaining purposes.
- */
- public BucketDefinition withQuota(final int quota) {
- if (quota < 100) {
- throw new IllegalArgumentException("Bucket quota cannot be less than 100MB!");
- }
- this.quota = quota;
- return this;
- }
-
- /**
- * Allows to disable creating a primary index for this bucket (enabled by default).
- *
- * @param create if false, a primary index will not be created.
- * @return this {@link BucketDefinition} for chaining purposes.
- */
- public BucketDefinition withPrimaryIndex(final boolean create) {
- this.queryPrimaryIndex = create;
- return this;
- }
-
- public String getName() {
- return name;
- }
-
- public boolean hasPrimaryIndex() {
- return queryPrimaryIndex;
- }
-
- public int getQuota() {
- return quota;
- }
-
-}
diff --git a/modules/couchbase/src/main/java/org/testcontainers/couchbase/CouchbaseContainer.java b/modules/couchbase/src/main/java/org/testcontainers/couchbase/CouchbaseContainer.java
index c65d1e09034..f5947c35ef4 100644
--- a/modules/couchbase/src/main/java/org/testcontainers/couchbase/CouchbaseContainer.java
+++ b/modules/couchbase/src/main/java/org/testcontainers/couchbase/CouchbaseContainer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020 Couchbase, Inc.
+ * Copyright (c) 2016 Couchbase, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,73 +15,94 @@
*/
package org.testcontainers.couchbase;
+import com.couchbase.client.core.utils.Base64;
+import com.couchbase.client.java.Bucket;
+import com.couchbase.client.java.CouchbaseCluster;
+import com.couchbase.client.java.cluster.*;
+import com.couchbase.client.java.env.CouchbaseEnvironment;
+import com.couchbase.client.java.env.DefaultCouchbaseEnvironment;
+import com.couchbase.client.java.query.Index;
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.api.model.ContainerNetwork;
-import okhttp3.Credentials;
-import okhttp3.FormBody;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
+import com.google.common.collect.Lists;
+import lombok.*;
+import org.apache.commons.compress.utils.Sets;
+import org.apache.commons.io.IOUtils;
+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.containers.wait.strategy.WaitAllStrategy;
import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.images.builder.Transferable;
+import org.testcontainers.utility.ThrowingFunction;
+import java.io.DataOutputStream;
import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.net.HttpURLConnection.HTTP_OK;
+import static org.testcontainers.couchbase.CouchbaseContainer.CouchbasePort.*;
/**
- * The couchbase container initializes and configures a Couchbase Server single node cluster.
+ * Based on Laurent Doguin version,
*
- * Note that it does not depend on a specific couchbase SDK, so it can be used with both the Java SDK 2 and 3 as well
- * as the Scala SDK 1 or newer. We recommend using the latest and greatest SDKs for the best experience.
+ * optimized by Tayeb Chlyah
*/
+@AllArgsConstructor
public class CouchbaseContainer extends GenericContainer {
- private static final int MGMT_PORT = 8091;
-
- private static final int MGMT_SSL_PORT = 18091;
+ private static final String DEFAULT_TAG = "5.5.1";
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("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 VIEW_PORT = 8092;
+ private static final int REQUIRED_DEFAULT_PASSWORD_LENGTH = 6;
- private static final int VIEW_SSL_PORT = 18092;
+ private String memoryQuota = "300";
- private static final int QUERY_PORT = 8093;
+ private String indexMemoryQuota = "300";
- private static final int QUERY_SSL_PORT = 18093;
+ private String clusterUsername = "Administrator";
- private static final int SEARCH_PORT = 8094;
+ private String clusterPassword = "password";
- private static final int SEARCH_SSL_PORT = 18094;
+ private boolean keyValue = true;
- private static final int KV_PORT = 11210;
+ @Getter
+ private boolean query = true;
- private static final int KV_SSL_PORT = 11207;
+ @Getter
+ private boolean index = true;
- private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("couchbase/server");
+ @Getter
+ private boolean primaryIndex = true;
- private static final String DEFAULT_TAG = "6.5.0";
+ @Getter
+ private boolean fts = false;
- private static final ObjectMapper MAPPER = new ObjectMapper();
+ @Getter(lazy = true)
+ private final CouchbaseEnvironment couchbaseEnvironment = createCouchbaseEnvironment();
- private static final OkHttpClient HTTP_CLIENT = new OkHttpClient();
+ @Getter(lazy = true)
+ private final CouchbaseCluster couchbaseCluster = createCouchbaseCluster();
- private String username = "Administrator";
+ private List newBuckets = new ArrayList<>();
- private String password = "password";
+ private String urlBase;
- private Set enabledServices = EnumSet.allOf(CouchbaseService.class);
-
- private final List buckets = new ArrayList<>();
+ private SocatContainer proxy;
/**
* Creates a new couchbase container with the default image and version.
@@ -109,343 +130,353 @@ public CouchbaseContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+
+ withNetwork(Network.SHARED);
+ setWaitStrategy(new HttpWaitStrategy().forPath("/ui/index.html"));
}
- /**
- * Set custom username and password for the admin user.
- *
- * @param username the admin username to use.
- * @param password the password for the admin user.
- * @return this {@link CouchbaseContainer} for chaining purposes.
- */
- public CouchbaseContainer withCredentials(final String username, final String password) {
- checkNotRunning();
- this.username = username;
- this.password = password;
- return this;
+ @Override
+ public Set getLivenessCheckPortNumbers() {
+ return Sets.newHashSet(getMappedPort(REST));
}
- public CouchbaseContainer withBucket(final BucketDefinition bucketDefinition) {
- checkNotRunning();
- this.buckets.add(bucketDefinition);
- return this;
+ @Override
+ protected void configure() {
+ if (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.");
+ }
}
- public CouchbaseContainer withEnabledServices(final CouchbaseService... enabled) {
- checkNotRunning();
- this.enabledServices = EnumSet.copyOf(Arrays.asList(enabled));
- return this;
+ @Override
+ @SneakyThrows
+ protected void doStart() {
+ startProxy(getNetworkAliases().get(0));
+ try {
+ super.doStart();
+ } catch (Throwable e) {
+ proxy.stop();
+ throw e;
+ }
}
- public final String getUsername() {
- return username;
+ @SneakyThrows
+ private void startProxy(String networkAlias) {
+ proxy = new SocatContainer().withNetwork(getNetwork());
+
+ for (CouchbasePort port : CouchbasePort.values()) {
+ if (port.isDynamic()) {
+ proxy.withTarget(port.getOriginalPort(), networkAlias);
+ } else {
+ proxy.addExposedPort(port.getOriginalPort());
+ }
+ }
+
+ proxy.setWaitStrategy(null);
+ proxy.start();
+
+ ExecCreateCmdResponse createCmdResponse = dockerClient
+ .execCreateCmd(proxy.getContainerId())
+ .withCmd(
+ "sh",
+ "-c",
+ Stream.of(CouchbasePort.values())
+ .map(port -> {
+ return "/usr/bin/socat " +
+ "TCP-LISTEN:" + port.getOriginalPort() + ",fork,reuseaddr " +
+ "TCP:" + networkAlias + ":" + getMappedPort(port);
+ })
+ .collect(Collectors.joining(" & ", "true", ""))
+ )
+ .exec();
+
+ dockerClient.execStartCmd(createCmdResponse.getId())
+ .start()
+ .awaitCompletion(10, TimeUnit.SECONDS);
}
- public final String getPassword() {
- return password;
+ @Override
+ public List getExposedPorts() {
+ return proxy.getExposedPorts();
}
- public int getBootstrapCarrierDirectPort() {
- return getMappedPort(KV_PORT);
+ @Override
+ public String getHost() {
+ return proxy.getHost();
}
- public int getBootstrapHttpDirectPort() {
- return getMappedPort(MGMT_PORT);
+ @Override
+ public Integer getMappedPort(int originalPort) {
+ return proxy.getMappedPort(originalPort);
}
- public String getConnectionString() {
- return String.format("couchbase://%s:%d", getHost(), getBootstrapCarrierDirectPort());
+ protected Integer getMappedPort(CouchbasePort port) {
+ return getMappedPort(port.getOriginalPort());
}
@Override
- protected void configure() {
- super.configure();
-
- WaitAllStrategy waitStrategy = new WaitAllStrategy();
-
- // Makes sure that all nodes in the cluster are healthy.
- waitStrategy = waitStrategy.withStrategy(
- new HttpWaitStrategy()
- .forPath("/pools/default")
- .forPort(MGMT_PORT)
- .withBasicCredentials(username, password)
- .forStatusCode(200)
- .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;
- }
- })
- );
-
- if (enabledServices.contains(CouchbaseService.QUERY)) {
- waitStrategy = waitStrategy.withStrategy(
- new HttpWaitStrategy()
- .forPath("/admin/ping")
- .forPort(QUERY_PORT)
- .withBasicCredentials(username, password)
- .forStatusCode(200)
- );
- }
-
- waitingFor(waitStrategy);
+ public List getBoundPortNumbers() {
+ return proxy.getBoundPortNumbers();
}
@Override
- protected void containerIsStarting(final InspectContainerResponse containerInfo) {
- logger().debug("Couchbase container is starting, performing configuration.");
-
- waitUntilNodeIsOnline();
- renameNode();
- initializeServices();
- configureAdminUser();
- configureExternalPorts();
- if (enabledServices.contains(CouchbaseService.INDEX)) {
- configureIndexer();
+ @SuppressWarnings({"unchecked", "ConstantConditions"})
+ public void stop() {
+ try {
+ stopCluster();
+ ((AtomicReference