diff --git a/modules/couchbase/pom.xml b/modules/couchbase/pom.xml
new file mode 100644
index 00000000000..7a0d5b87145
--- /dev/null
+++ b/modules/couchbase/pom.xml
@@ -0,0 +1,30 @@
+
+
+ 4.0.0
+
+
+ org.testcontainers
+ testcontainers-parent
+ 1.1.8-SNAPSHOT
+ ../../pom.xml
+
+
+ couchbase
+ TestContainers :: Couchbase
+
+
+
+ ${project.groupId}
+ testcontainers
+ ${project.version}
+
+
+
+
+ com.couchbase.client
+ java-client
+ 2.4.0
+ provided
+
+
+
diff --git a/modules/couchbase/src/main/java/org/testcontainers/containers/CouchbaseContainer.java b/modules/couchbase/src/main/java/org/testcontainers/containers/CouchbaseContainer.java
new file mode 100644
index 00000000000..b8c8623d00a
--- /dev/null
+++ b/modules/couchbase/src/main/java/org/testcontainers/containers/CouchbaseContainer.java
@@ -0,0 +1,278 @@
+/*
+ * 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.
+ * 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.containers;
+
+import com.couchbase.client.core.message.config.RestApiResponse;
+import com.couchbase.client.core.utils.Base64;
+import com.couchbase.client.deps.io.netty.handler.codec.http.HttpHeaders;
+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.cluster.api.ClusterApiClient;
+import com.couchbase.client.java.cluster.api.RestBuilder;
+import com.couchbase.client.java.document.json.JsonArray;
+import com.couchbase.client.java.env.CouchbaseEnvironment;
+import com.couchbase.client.java.env.DefaultCouchbaseEnvironment;
+import com.couchbase.client.java.query.Index;
+import com.couchbase.client.java.query.N1qlQuery;
+import lombok.Cleanup;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.traits.LinkableContainer;
+import org.testcontainers.containers.wait.HttpWaitStrategy;
+import org.testcontainers.shaded.com.github.dockerjava.api.command.InspectContainerResponse;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Laurent Doguin
+ */
+public class CouchbaseContainer> extends GenericContainer {
+
+ private String memoryQuota = "400";
+
+ private String indexMemoryQuota = "400";
+
+ private String clusterUsername = "Administrator";
+
+ private String clusterPassword = "password";
+
+ private Boolean keyValue = true;
+
+ private Boolean query = true;
+
+ private Boolean index = true;
+
+ private Boolean fts = true;
+
+ private Boolean beerSample = false;
+
+ private Boolean travelSample = false;
+
+ private Boolean gamesIMSample = false;
+
+ private CouchbaseEnvironment couchbaseEnvironment;
+
+ private CouchbaseCluster couchbaseCluster;
+
+ private List newBuckets = new ArrayList<>();
+
+ private String urlBase;
+
+ public CouchbaseContainer() {
+ super("couchbase/server:4.5.1");
+ }
+
+ public CouchbaseContainer(String containerName) {
+ super(containerName);
+ }
+
+ @Override
+ protected Integer getLivenessCheckPort() {
+ return getMappedPort(8091);
+ }
+
+ @Override
+ protected void configure() {
+ addFixedExposedPort(8092, 8092);
+ addFixedExposedPort(8093, 8093);
+ addFixedExposedPort(8094, 8094);
+ addFixedExposedPort(8095, 8095);
+ addFixedExposedPort(11211, 11211);
+ addFixedExposedPort(18092, 18092);
+ addFixedExposedPort(18093, 18093);
+ addExposedPorts(11210, 11207, 8091, 18091);
+ setWaitStrategy(new HttpWaitStrategy().forPath("/ui/index.html#/"));
+ }
+
+ public CouchbaseEnvironment getCouchbaseEnvironnement() {
+ if (couchbaseEnvironment == null) {
+ couchbaseEnvironment = DefaultCouchbaseEnvironment.builder()
+ .bootstrapCarrierDirectPort(getMappedPort(11210))
+ .bootstrapCarrierSslPort(getMappedPort(11207))
+ .bootstrapHttpDirectPort(getMappedPort(8091))
+ .bootstrapHttpSslPort(getMappedPort(18091))
+ .build();
+ }
+ return couchbaseEnvironment;
+ }
+
+ public CouchbaseCluster getCouchbaseCluster() {
+ if (couchbaseCluster == null) {
+ couchbaseCluster = CouchbaseCluster.create(getCouchbaseEnvironnement(), getContainerIpAddress());
+ }
+ return couchbaseCluster;
+ }
+
+ public SELF withClusterUsername(String username) {
+ this.clusterUsername = username;
+ return self();
+ }
+
+ public SELF withClusterPassword(String password) {
+ this.clusterPassword = password;
+ return self();
+ }
+
+ public SELF withMemoryQuota(String memoryQuota) {
+ this.memoryQuota = memoryQuota;
+ return self();
+ }
+
+ public SELF withIndexMemoryQuota(String indexMemoryQuota) {
+ this.indexMemoryQuota = indexMemoryQuota;
+ return self();
+ }
+
+ public SELF withKeyValue(Boolean withKV) {
+ this.keyValue = withKV;
+ return self();
+ }
+
+ public SELF withIndex(Boolean withIndex) {
+ this.index = withIndex;
+ return self();
+ }
+
+ public SELF withQuery(Boolean withQuery) {
+ this.query = withQuery;
+ return self();
+ }
+
+ public SELF withFTS(Boolean withFTS) {
+ this.fts = withFTS;
+ return self();
+ }
+
+ public SELF withTravelSample(Boolean withTravelSample) {
+ this.travelSample = withTravelSample;
+ return self();
+ }
+
+ public SELF withBeerSample(Boolean withBeerSample) {
+ this.beerSample = withBeerSample;
+ return self();
+ }
+
+ public SELF withGamesIMSample(Boolean withGamesIMSample) {
+ this.gamesIMSample = withGamesIMSample;
+ return self();
+ }
+
+ public SELF withNewBucket(BucketSettings bucketSettings) {
+ newBuckets.add(bucketSettings);
+ return self();
+ }
+
+ @Override
+ protected void containerIsStarted(InspectContainerResponse containerInfo) {
+ urlBase = String.format("http://%s:%s", getContainerIpAddress(), getMappedPort(8091));
+ try {
+ String poolURL = "/pools/default";
+ String poolPayload = "memoryQuota=" + URLEncoder.encode(memoryQuota, "UTF-8") + "&indexMemoryQuota=" + URLEncoder.encode(indexMemoryQuota, "UTF-8");
+
+ String setupServicesURL = "/node/controller/setupServices";
+ StringBuilder servicePayloadBuilder = new StringBuilder();
+ if (keyValue) {
+ servicePayloadBuilder.append("kv,");
+ }
+ if (query) {
+ servicePayloadBuilder.append("n1ql,");
+ }
+ if (index) {
+ servicePayloadBuilder.append("index,");
+ }
+ if (fts) {
+ servicePayloadBuilder.append("fts,");
+ }
+ String setupServiceContent = "services=" + URLEncoder.encode(servicePayloadBuilder.toString(), "UTF-8");
+
+ String webSettingsURL = "/settings/web";
+ String webSettingsContent = "username=" + URLEncoder.encode(clusterUsername, "UTF-8") + "&password=" + URLEncoder.encode(clusterPassword, "UTF-8") + "&port=8091";
+
+ String bucketURL = "/sampleBuckets/install";
+
+ StringBuilder sampleBucketPayloadBuilder = new StringBuilder();
+ sampleBucketPayloadBuilder.append('[');
+ if (travelSample) {
+ sampleBucketPayloadBuilder.append("\"travel-sample\",");
+ }
+ if (beerSample) {
+ sampleBucketPayloadBuilder.append("\"beer-sample\",");
+ }
+ if (gamesIMSample) {
+ sampleBucketPayloadBuilder.append("\"gamesim-sample\",");
+ }
+ sampleBucketPayloadBuilder.append(']');
+
+ callCouchbaseRestAPI(poolURL, poolPayload);
+ callCouchbaseRestAPI(setupServicesURL, setupServiceContent);
+ callCouchbaseRestAPI(webSettingsURL, webSettingsContent);
+ callCouchbaseRestAPI(bucketURL, sampleBucketPayloadBuilder.toString());
+ callCouchbaseRestAPI("/settings/indexes", "indexerThreads=0&logLevel=info&maxRollbackPoints=5&storageMode=memory_optimized");
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void createBucket(BucketSettings bucketSetting, Boolean createIndex){
+ BucketSettings bucketSettings = getCouchbaseCluster().clusterManager(clusterUsername, clusterPassword).insertBucket(bucketSetting);
+ // allow some time for the query service to come up
+ try {
+ Thread.sleep(5000);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ if (createIndex) {
+ getCouchbaseCluster().openBucket().query(Index.createPrimaryIndex().on(bucketSetting.name()));
+ }
+ }
+
+ protected void callCouchbaseRestAPI(String url, String payload) throws IOException {
+ String fullUrl = 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 = Base64.encode((clusterUsername + ":" + clusterPassword).getBytes("UTF-8"));
+ httpConnection.setRequestProperty("Authorization", "Basic " + encoded);
+ @Cleanup DataOutputStream out = new DataOutputStream(httpConnection.getOutputStream());
+ out.writeBytes(payload);
+ out.flush();
+
+ httpConnection.getResponseCode();
+ httpConnection.disconnect();
+ }
+
+ @Override
+ public void start() {
+ super.start();
+ if (!newBuckets.isEmpty()) {
+ for (BucketSettings bucketSetting : newBuckets) {
+ createBucket(bucketSetting, index);
+ }
+ }
+ }
+
+}
+
diff --git a/modules/couchbase/src/main/java/org/testcontainers/containers/CouchbaseWaitStrategy.java b/modules/couchbase/src/main/java/org/testcontainers/containers/CouchbaseWaitStrategy.java
new file mode 100644
index 00000000000..277e7333fb3
--- /dev/null
+++ b/modules/couchbase/src/main/java/org/testcontainers/containers/CouchbaseWaitStrategy.java
@@ -0,0 +1,157 @@
+/*
+ * 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.
+ * 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.containers;
+
+import com.couchbase.client.deps.com.fasterxml.jackson.databind.JsonNode;
+import com.couchbase.client.deps.com.fasterxml.jackson.databind.ObjectMapper;
+import org.rnorth.ducttape.TimeoutException;
+import org.testcontainers.shaded.com.google.common.base.Strings;
+import org.testcontainers.shaded.com.google.common.io.BaseEncoding;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+import java.util.concurrent.TimeUnit;
+
+import static org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess;
+
+/**
+ * Created by ldoguin on 18/07/16.
+ */
+public class CouchbaseWaitStrategy extends GenericContainer.AbstractWaitStrategy {
+ /**
+ * Authorization HTTP header.
+ */
+ private static final String HEADER_AUTHORIZATION = "Authorization";
+
+ /**
+ * Basic Authorization scheme prefix.
+ */
+ private static final String AUTH_BASIC = "Basic ";
+
+ private String path = "/pools/default/";
+ private int statusCode = HttpURLConnection.HTTP_OK;
+ private boolean tlsEnabled;
+ private String username;
+ private String password;
+ private ObjectMapper om = new ObjectMapper();
+
+ /**
+ * Indicates that the status check should use HTTPS.
+ *
+ * @return this
+ */
+ public CouchbaseWaitStrategy usingTls() {
+ this.tlsEnabled = true;
+ return this;
+ }
+
+ /**
+ * Authenticate with HTTP Basic Authorization credentials.
+ *
+ * @param username the username
+ * @param password the password
+ * @return this
+ */
+ public CouchbaseWaitStrategy withBasicCredentials(String username, String password) {
+ this.username = username;
+ this.password = password;
+ return this;
+ }
+
+ @Override
+ protected void waitUntilReady() {
+ final Integer livenessCheckPort = getLivenessCheckPort();
+ if (null == livenessCheckPort) {
+ logger().warn("No exposed ports or mapped ports - cannot wait for status");
+ return;
+ }
+
+ final String uri = buildLivenessUri(livenessCheckPort).toString();
+ logger().info("Waiting for {} seconds for URL: {}", startupTimeout.getSeconds(), uri);
+
+ // try to connect to the URL
+ try {
+ retryUntilSuccess((int) startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> {
+ getRateLimiter().doWhenReady(() -> {
+ try {
+ final HttpURLConnection connection = (HttpURLConnection) new URL(uri).openConnection();
+
+ // authenticate
+ if (!Strings.isNullOrEmpty(username)) {
+ connection.setRequestProperty(HEADER_AUTHORIZATION, buildAuthString(username, password));
+ connection.setUseCaches(false);
+ }
+
+ connection.setRequestMethod("GET");
+ connection.connect();
+
+ if (statusCode != connection.getResponseCode()) {
+ throw new RuntimeException(String.format("HTTP response code was: %s",
+ connection.getResponseCode()));
+ }
+
+ // Specific Couchbase wait strategy to be sure the node is online and healthy
+ JsonNode node = om.readTree(connection.getInputStream());
+ JsonNode statusNode = node.at("/nodes/0/status");
+ String status = statusNode.asText();
+ if (!"healthy".equals(status)){
+ throw new RuntimeException(String.format("Couchbase Node status was: %s", status));
+ }
+
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ return true;
+ });
+
+ } catch (TimeoutException e) {
+ throw new ContainerLaunchException(String.format(
+ "Timed out waiting for URL to be accessible (%s should return HTTP %s)", uri, statusCode));
+ }
+ }
+
+ /**
+ * Build the URI on which to check if the container is ready.
+ *
+ * @param livenessCheckPort the liveness port
+ * @return the liveness URI
+ */
+ private URI buildLivenessUri(int livenessCheckPort) {
+ final String scheme = (tlsEnabled ? "https" : "http") + "://";
+ final String host = container.getContainerIpAddress();
+
+ final String portSuffix;
+ if ((tlsEnabled && 443 == livenessCheckPort) || (!tlsEnabled && 80 == livenessCheckPort)) {
+ portSuffix = "";
+ } else {
+ portSuffix = ":" + String.valueOf(livenessCheckPort);
+ }
+
+ return URI.create(scheme + host + portSuffix + path);
+ }
+
+ /**
+ * @param username the username
+ * @param password the password
+ * @return a basic authentication string for the given credentials
+ */
+ private String buildAuthString(String username, String password) {
+ return AUTH_BASIC + BaseEncoding.base64().encode((username + ":" + password).getBytes());
+ }
+}
diff --git a/modules/couchbase/src/test/java/org/testcontainers/junit/SimpleCouchbaseTest.java b/modules/couchbase/src/test/java/org/testcontainers/junit/SimpleCouchbaseTest.java
new file mode 100644
index 00000000000..d76bcd10692
--- /dev/null
+++ b/modules/couchbase/src/test/java/org/testcontainers/junit/SimpleCouchbaseTest.java
@@ -0,0 +1,73 @@
+package org.testcontainers.junit;
+
+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.ClusterManager;
+import com.couchbase.client.java.cluster.DefaultBucketSettings;
+import com.couchbase.client.java.document.JsonDocument;
+import com.couchbase.client.java.document.json.JsonObject;
+import com.couchbase.client.java.query.N1qlParams;
+import com.couchbase.client.java.query.N1qlQuery;
+import com.couchbase.client.java.query.N1qlQueryResult;
+import com.couchbase.client.java.query.consistency.ScanConsistency;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.testcontainers.containers.CouchbaseContainer;
+
+import java.io.*;
+import java.net.URLConnection;
+
+import static org.rnorth.visibleassertions.VisibleAssertions.*;
+
+
+/**
+ * @author ldoguin
+ */
+public class SimpleCouchbaseTest {
+
+ public static final String clusterUser = "Administrator";
+ public static final String clusterPassword = "password";
+
+ @Rule
+ public CouchbaseContainer couchbase = new CouchbaseContainer()
+ .withIndex(true)
+ .withQuery(true)
+ .withTravelSample(true)
+ .withClusterUsername(clusterUser)
+ .withClusterPassword(clusterPassword)
+ .withNewBucket(DefaultBucketSettings.builder().enableFlush(true).name("default").quota(100).replicas(0).type(BucketType.COUCHBASE).build());
+
+
+ @Test
+ public void testSimple() throws Exception {
+ CouchbaseCluster cluster = couchbase.getCouchbaseCluster();
+
+ // Open default bucket
+ Bucket defaultBucket = cluster.openBucket();
+ Assert.assertNotNull(defaultBucket);
+ // Open travel sample bucket
+ Bucket travelSample = cluster.openBucket("travel-sample");
+ Assert.assertNotNull(travelSample);
+
+ // verify Credentials
+ ClusterManager manager = cluster.clusterManager(clusterUser, clusterPassword);
+ Assert.assertNotNull(manager);
+
+ // Verify KV, Query and Index Service
+ JsonObject object = JsonObject.create();
+ object.put("key","value");
+ object.put("is","awesome");
+ JsonDocument doc = JsonDocument.create("testDoc");
+ defaultBucket.insert(doc);
+ N1qlParams params = N1qlParams.build().consistency(ScanConsistency.STATEMENT_PLUS);
+ N1qlQuery q = N1qlQuery.simple("Select * from `default` limit 1", params);
+
+ N1qlQueryResult result = travelSample.query(q);
+ Assert.assertEquals(1, result.allRows().size());
+
+ }
+
+}
diff --git a/modules/couchbase/src/test/resources/logback-test.xml b/modules/couchbase/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..ed0e5b2659e
--- /dev/null
+++ b/modules/couchbase/src/test/resources/logback-test.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PROFILER
+ DENY
+
+
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index c6a5b3a85d5..915189ec2bc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -168,6 +168,7 @@
modules/nginx
modules/mariadb
modules/jdbc-test
+ modules/couchbase