diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..23927c1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,68 @@ +FROM ubuntu:14.04 + +MAINTAINER Couchbase Docker Team + +# Install dependencies: +# runit: for container process management +# wget: for downloading .deb +# python-httplib2: used by CLI tools +# chrpath: for fixing curl, below +# Additional dependencies for system commands used by cbcollect_info: +# lsof: lsof +# lshw: lshw +# sysstat: iostat, sar, mpstat +# net-tools: ifconfig, arp, netstat +# numactl: numactl +RUN apt-get update && \ + apt-get install -yq runit wget python-httplib2 chrpath \ + lsof lshw sysstat net-tools numactl && \ + apt-get autoremove && apt-get clean && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +ENV CB_VERSION=4.5.0 \ + CB_RELEASE_URL=http://packages.couchbase.com/releases \ + CB_PACKAGE=couchbase-server-enterprise_4.5.0-ubuntu14.04_amd64.deb \ + CB_SHA256=441398302210c0d73f27bdab741b471fc9da116bf45f521b314345f04560716e \ + PATH=$PATH:/opt/couchbase/bin:/opt/couchbase/bin/tools:/opt/couchbase/bin/install + +# Create Couchbase user with UID 1000 (necessary to match default +# boot2docker UID) +RUN groupadd -g 1000 couchbase && useradd couchbase -u 1000 -g couchbase -M + +# Install couchbase +RUN wget -N $CB_RELEASE_URL/$CB_VERSION/$CB_PACKAGE && \ + echo "$CB_SHA256 $CB_PACKAGE" | sha256sum -c - && \ + dpkg -i ./$CB_PACKAGE && rm -f ./$CB_PACKAGE && \ + sleep 10 && /opt/couchbase/bin/couchbase-cli cluster-init -c $HOSTNAME:8091 --cluster-username=Administrator --cluster-password=password --cluster-port=8091 --cluster-ramsize=384 --cluster-index-ramsize=384 --services=data,index,query && /opt/couchbase/bin/couchbase-cli bucket-create -c $HOSTNAME:8091 --bucket=default --bucket-type=couchbase --bucket-port=11211 --bucket-ramsize=100 --bucket-replica=0 -u Administrator -p password && /opt/couchbase/bin/cbdocloader -n $HOSTNAME:8091 -u Administrator -p password -b beer-sample /opt/couchbase/samples/beer-sample.zip + + +# Add runit script for couchbase-server +COPY scripts/run /etc/service/couchbase-server/run + +# Add dummy script for commands invoked by cbcollect_info that +# make no sense in a Docker container +COPY scripts/dummy.sh /usr/local/bin/ +RUN ln -s dummy.sh /usr/local/bin/iptables-save && \ + ln -s dummy.sh /usr/local/bin/lvdisplay && \ + ln -s dummy.sh /usr/local/bin/vgdisplay && \ + ln -s dummy.sh /usr/local/bin/pvdisplay + +# Fix curl RPATH +RUN chrpath -r '$ORIGIN/../lib' /opt/couchbase/bin/curl + +# Add bootstrap script +COPY scripts/entrypoint.sh / +ENTRYPOINT ["/entrypoint.sh"] +CMD ["couchbase-server"] + +# 8091: Couchbase Web console, REST/HTTP interface +# 8092: Views, queries, XDCR +# 8093: Query services (4.0+) +# 8094: Full-text Serarch (4.5+) +# 11207: Smart client library data node access (SSL) +# 11210: Smart client library/moxi data node access +# 11211: Legacy non-smart client library data node access +# 18091: Couchbase Web console, REST/HTTP interface (SSL) +# 18092: Views, query, XDCR (SSL) +# 18093: Query services (SSL) (4.0+) +EXPOSE 8091 8092 8093 8094 11207 11210 11211 18091 18092 18093 diff --git a/README.md b/README.md index a9d521b..4b2ba29 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,18 @@ # couchbase-test-docker -How to test Couchbase with a Docker container + + +JUnit test example with Couchbase in a Docker container and [TestContainers](http://testcontainers.viewdocs.io/testcontainers-java/). + +TestContainers allow you to run a container before tests, either for all the class or each methods. This example runs a container with Couchbase Server preconfigured and loaded with the beer-sample. Than test the beer-sample. To run this test successfuly you need to build the DockerFile at the root with the tag 'mycouchbase:latest'. + +## Building the container + + docker build -t mycouchbase:latest + +## Running the Rest + + gradle build + +## How it works + +The [GenericContainer](http://testcontainers.viewdocs.io/testcontainers-java/usage/generic_containers/) is used with a custom wait strategy. It wait for the HTTP endpoint `/pools/default` to be accessible and for the node to have the `healthy` status. \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..4674dfb --- /dev/null +++ b/build.gradle @@ -0,0 +1,28 @@ +apply plugin: 'java' +apply plugin: 'eclipse' + +sourceCompatibility = 1.8 +version = '1.0' +jar { + manifest { + attributes 'Implementation-Title': 'Couchbase Test Demo with Docker', + 'Implementation-Version': version + } +} + +repositories { + mavenCentral() + jcenter() + maven { + url "https://jitpack.io" + } +} + + +dependencies { + compile "org.testcontainers:testcontainers:1.1.0" + testCompile "com.couchbase.client:java-client:2.2.5", + "junit:junit:4.12" + runtime "org.slf4j:slf4j-simple:1.7.12" + +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..30d399d Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ad0be41 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Jul 19 11:30:56 CEST 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..91a7e26 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/scripts/dummy.sh b/scripts/dummy.sh new file mode 100755 index 0000000..0091b80 --- /dev/null +++ b/scripts/dummy.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +echo "Running in Docker container - $0 not available" + diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100755 index 0000000..7da0653 --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +[[ "$1" == "couchbase-server" ]] && { + echo "Starting Couchbase Server -- Web UI available at http://:8091" + exec /usr/sbin/runsvdir-start +} + +exec "$@" diff --git a/scripts/run b/scripts/run new file mode 100755 index 0000000..25caa10 --- /dev/null +++ b/scripts/run @@ -0,0 +1,15 @@ +#!/bin/sh + +exec 2>&1 + +# Create directories where couchbase stores its data +cd /opt/couchbase +mkdir -p var/lib/couchbase \ + var/lib/couchbase/config \ + var/lib/couchbase/data \ + var/lib/couchbase/stats \ + var/lib/couchbase/logs \ + var/lib/moxi + +chown -R couchbase:couchbase var +exec chpst -ucouchbase /opt/couchbase/bin/couchbase-server -- -noinput diff --git a/src/test/java/com/couchbase/CouchbaseWaitStrategy.java b/src/test/java/com/couchbase/CouchbaseWaitStrategy.java new file mode 100644 index 0000000..ef660c4 --- /dev/null +++ b/src/test/java/com/couchbase/CouchbaseWaitStrategy.java @@ -0,0 +1,144 @@ +package com.couchbase; + +import com.couchbase.client.deps.com.fasterxml.jackson.databind.JsonNode; +import com.couchbase.client.deps.com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Strings; +import com.google.common.io.BaseEncoding; +import org.rnorth.ducttape.TimeoutException; +import org.testcontainers.containers.ContainerLaunchException; +import org.testcontainers.containers.GenericContainer; + +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/src/test/java/com/couchbase/ExampleTest.java b/src/test/java/com/couchbase/ExampleTest.java new file mode 100644 index 0000000..280fed3 --- /dev/null +++ b/src/test/java/com/couchbase/ExampleTest.java @@ -0,0 +1,43 @@ +package com.couchbase; + +import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.CouchbaseCluster; +import com.couchbase.client.java.cluster.ClusterManager; +import com.couchbase.client.java.env.CouchbaseEnvironment; +import com.couchbase.client.java.env.DefaultCouchbaseEnvironment; +import org.junit.ClassRule; +import org.junit.Test;; +import org.testcontainers.containers.GenericContainer; + +import static org.junit.Assert.assertTrue; + +/** + * Created by ldoguin on 15/07/16. + */ +public class ExampleTest { + + @ClassRule + public static GenericContainer couchbase = + new GenericContainer("mycouchbase:latest") + .withExposedPorts(8091, 8092, 8093, 8094, 11207, 11210, 11211, 18091, 18092, 18093) + .waitingFor(new CouchbaseWaitStrategy()); + + + @Test + public void beerBucketTest() throws InterruptedException { + CouchbaseEnvironment env = DefaultCouchbaseEnvironment.builder() + .bootstrapCarrierDirectPort(couchbase.getMappedPort(11210)) + .bootstrapCarrierSslPort(couchbase.getMappedPort(11207)) + .bootstrapHttpDirectPort(couchbase.getMappedPort(8091)) + .bootstrapHttpSslPort(couchbase.getMappedPort(18091)) + .queryPort(couchbase.getMappedPort(8093)) + .build(); + CouchbaseCluster cc = CouchbaseCluster.create(env); + ClusterManager cm = cc.clusterManager("Administrator", "password"); + assertTrue(cm.hasBucket("beer-sample")); + Bucket bucket = cc.openBucket("beer-sample"); + assertTrue(bucket.exists("21st_amendment_brewery_cafe")); + bucket.close(); + } + +}