Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Couchbase module #688

Merged
merged 19 commits into from
Jun 8, 2018
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/couchbase/AUTHORS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Tayeb Chlyah <[email protected]>
59 changes: 59 additions & 0 deletions modules/couchbase/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<img src="https://cdn.worldvectorlogo.com/logos/couchbase.svg" width="300" />

# TestContainers Couchbase Module
Testcontainers module for Couchbase. [Couchbase](https://www.couchbase.com/) is a Document oriented NoSQL database.

## Usage example

Running Couchbase as a stand-in in a test:

### Create you own bucket

```java
public class SomeTest {

@Rule
public CouchbaseContainer couchbase = new CouchbaseContainer()
.withNewBucket(DefaultBucketSettings.builder()
.enableFlush(true)
.name('bucket-name')
.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 testContainer 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 :
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI we usually use just word "container"

- **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.
6 changes: 6 additions & 0 deletions modules/couchbase/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
description = "Testcontainers :: Couchbase"

dependencies {
compile project(':testcontainers')
compile 'com.couchbase.client:java-client:2.5.7'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

compileOnly perhaps?

Copy link
Contributor Author

@tchlyah tchlyah May 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couchbase java client is actually used, so if the client doesn't add this library, it won't run.

It is also used in test, it will force me to add testCompile.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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.DefaultBucketSettings;
import com.couchbase.client.java.query.N1qlParams;
import com.couchbase.client.java.query.N1qlQuery;
import com.couchbase.client.java.query.consistency.ScanConsistency;
import lombok.Getter;
import org.junit.After;

/**
* @author ctayeb
*/
public abstract class AbstractCouchbaseTest {

public static final String TEST_BUCKET = "test";

public static final String DEFAULT_PASSWORD = "password";

@Getter(lazy = true)
private final static CouchbaseContainer couchbaseContainer = initCouchbaseContainer();

@Getter(lazy = true)
private final static Bucket bucket = openBucket(TEST_BUCKET, DEFAULT_PASSWORD);

@After
public void clear() {
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();
}
}

private static CouchbaseContainer initCouchbaseContainer() {
CouchbaseContainer couchbaseContainer = new CouchbaseContainer()
.withNewBucket(DefaultBucketSettings.builder()
.enableFlush(true)
.name(TEST_BUCKET)
.password(DEFAULT_PASSWORD)
.quota(100)
.replicas(0)
.type(BucketType.COUCHBASE)
.build());
couchbaseContainer.start();
return couchbaseContainer;
}

private static Bucket openBucket(String bucketName, String password) {
CouchbaseCluster cluster = getCouchbaseContainer().getCouchbaseCluster();
Bucket bucket = cluster.openBucket(bucketName, password);
Runtime.getRuntime().addShutdownHook(new Thread(bucket::close));
return bucket;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
/*
* 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.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 lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.experimental.Wither;
import org.apache.commons.compress.utils.Sets;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.HttpWaitStrategy;

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.Collections;
import java.util.List;
import java.util.Set;

/**
* Based on Laurent Doguin version
* <p>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JavaDoc task will fail

* Optimized by ctayeb
*/
@AllArgsConstructor
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it really needed?

Copy link
Contributor Author

@tchlyah tchlyah May 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is needed for lombok @Wither

public class CouchbaseContainer<SELF extends CouchbaseContainer<SELF>> extends GenericContainer<SELF> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a final container, you can use:

class CouchbaseContainer extends GenericContainer<CouchbaseContainer>


@Wither
private String memoryQuota = "300";

@Wither
private String indexMemoryQuota = "300";

@Wither
private String clusterUsername = "Administrator";

@Wither
private String clusterPassword = "password";

@Wither
private boolean keyValue = true;

@Getter
@Wither
private boolean query = true;

@Getter
@Wither
private boolean index = true;

@Getter
@Wither
private boolean primaryIndex = true;

@Getter
@Wither
private boolean fts = false;

@Wither
private boolean beerSample = false;

@Wither
private boolean travelSample = false;

@Wither
private boolean gamesIMSample = false;

@Getter(lazy = true)
private final CouchbaseEnvironment couchbaseEnvironment = createCouchbaseEnvironment();

@Getter(lazy = true)
private final CouchbaseCluster couchbaseCluster = createCouchbaseCluster();

private List<BucketSettings> newBuckets = new ArrayList<>();

private String urlBase;

public CouchbaseContainer() {
super("couchbase/server:latest");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you please fix the version?

}

public CouchbaseContainer(String containerName) {
super(containerName);
}

@Override
protected Integer getLivenessCheckPort() {
return getMappedPort(8091);
}

@Override
public Set<Integer> getLivenessCheckPortNumbers() {
return Sets.newHashSet(getLivenessCheckPort());
}

@Override
protected void configure() {
// Configurable ports
addExposedPorts(11210, 11207, 8091, 18091);

// Non configurable ports
addFixedExposedPort(8092, 8092);
addFixedExposedPort(8093, 8093);
addFixedExposedPort(8094, 8094);
addFixedExposedPort(8095, 8095);
addFixedExposedPort(18092, 18092);
addFixedExposedPort(18093, 18093);
setWaitStrategy(new HttpWaitStrategy().forPath("/ui/index.html#/"));
}

public SELF withNewBucket(BucketSettings bucketSettings) {
newBuckets.add(bucketSettings);
return self();
}

public void initCluster() {
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());

CouchbaseWaitStrategy s = new CouchbaseWaitStrategy();
s.withBasicCredentials(clusterUsername, clusterPassword);
s.waitUntilReady(this);
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 primaryIndex) {
ClusterManager clusterManager = getCouchbaseCluster().clusterManager(clusterUsername, clusterPassword);
// Insert Bucket
BucketSettings bucketSettings = clusterManager.insertBucket(bucketSetting);
// Insert Bucket admin user
UserSettings userSettings = UserSettings.build()
.password(bucketSetting.password())
.roles(Collections.singletonList(new UserRole("bucket_admin", bucketSetting.name())));
try {
clusterManager.upsertUser(AuthDomain.LOCAL, bucketSetting.name(), userSettings);
} catch (Exception e) {
logger().warn("Unable to insert user '" + bucketSetting.name() + "', maybe you are using older version");
}
if (index) {
Bucket bucket = getCouchbaseCluster().openBucket(bucketSettings.name(), bucketSettings.password());
new CouchbaseQueryServiceWaitStrategy(bucket).waitUntilReady(this);
if (primaryIndex) {
bucket.query(Index.createPrimaryIndex().on(bucketSetting.name()));
}
}
}

public void callCouchbaseRestAPI(String url, String payload) throws IOException {
String fullUrl = urlBase + url;
HttpURLConnection httpConnection = (HttpURLConnection) ((new URL(fullUrl).openConnection()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please wrap with try-with-resources

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);
DataOutputStream out = new DataOutputStream(httpConnection.getOutputStream());
out.writeBytes(payload);
out.flush();
out.close();
httpConnection.getResponseCode();
httpConnection.disconnect();
}

@Override
public void start() {
super.start();
if (!newBuckets.isEmpty()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

containerIsStarted is probably a better method to do this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes indeed. For the record, I never tried to optimize the original code, just make it work 😉

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ok, sorry :) Thanks for changing it 👍

for (BucketSettings bucketSetting : newBuckets) {
createBucket(bucketSetting, primaryIndex);
}
}
}

private CouchbaseCluster createCouchbaseCluster() {
return CouchbaseCluster.create(getCouchbaseEnvironment(), getContainerIpAddress());
}

private DefaultCouchbaseEnvironment createCouchbaseEnvironment() {
initCluster();
return DefaultCouchbaseEnvironment.builder()
.bootstrapCarrierDirectPort(getMappedPort(11210))
.bootstrapCarrierSslPort(getMappedPort(11207))
.bootstrapHttpDirectPort(getMappedPort(8091))
.bootstrapHttpSslPort(getMappedPort(18091))
.build();
}
}
Loading