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