diff --git a/Dockerfile b/Dockerfile index 9cc1cf7..b637958 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,7 +58,6 @@ ADD root / # Container setup # RUN chown cassandra:0 /etc/cassandra/cassandra.yaml && \ -# sed -ri 's/(^authorizer:).*/\1 CassandraAuthorizer/' /etc/cassandra/cassandra.yaml && \ # chown -R cassandra.0 /var/lib/cassandra/ && \ # Loosen permission bits to avoid problems running container with arbitrary UID # chmod -R g+rwx /var/lib/cassandra diff --git a/root/usr/bin/run-cassandra b/root/usr/bin/run-cassandra index 5c2bd62..6e1ea49 100755 --- a/root/usr/bin/run-cassandra +++ b/root/usr/bin/run-cassandra @@ -8,6 +8,12 @@ save_env_config_vars if [ "$CASSANDRA_ADMIN_PASSWORD" ]; then create_admin_user + turn_authorization_on +# so far this is not working because cassandra-env.sh file is not modifiable (sits in scripts directory) +# turn_on_jmx_authentication +# the admin password is not mandatory yet, just uncomment in case of change +#else +# usage "CASSANDRA_ADMIN_PASSWORD has to be set" fi exec cassandra -f diff --git a/root/usr/share/container-scripts/cassandra/common.sh b/root/usr/share/container-scripts/cassandra/common.sh index e4ddfc1..c82f063 100644 --- a/root/usr/share/container-scripts/cassandra/common.sh +++ b/root/usr/share/container-scripts/cassandra/common.sh @@ -8,6 +8,24 @@ CASSANDRA_CONF_FILE="cassandra.yaml" HOSTNAME=$(cat /proc/sys/kernel/hostname) #IP_ADDRESS=$(cat /etc/hosts | grep $HOSTNAME | awk '{print $1}' | head -n 1) +# usage prints info about required enviromental variables +# if $1 is passed, prints error message containing $1 +function usage() { + if [ $# == 1 ]; then + echo >&2 "error: $1" + fi + + echo " +You must specify the following environment variables: + CASSANDRA_ADMIN_PASSWORD" + + echo " +For more information see /usr/share/container-scripts/cassandra/README.md +within the container or visit https://github.com/sclorg/cassandra-container/." + + exit 1 +} + # update cassandra config file (cassandra.yaml) based on the environment varibales # set by the user function save_env_config_vars() { @@ -94,8 +112,43 @@ function create_admin_user() { nodetool stopdaemon 2>/dev/null # echo server stopped + # optionaly create a cqlshrc file with the login information + # NOT SUPPORTED YET +# if [ ! -d "/var/lib/cassandra/.cassandra" ]; then +# mkdir /var/lib/cassandra/.cassandra +# fi +# cat << 'EOF' >> /var/lib/cassandra/.cassandra/cqlshrc +# [authentication] +# username = admin +# password = "$CASSANDRA_ADMIN_PASSWORD" +# EOF +# chmod 440 /var/lib/cassandra/.cassandra/cqlshrc +# echo cqlshrc file with the credentials created + # hide the admin password unset CASSANDRA_ADMIN_PASSWORD # echo password var dropped } + +# turn on the authorization +function turn_authorization_on() { + # change the config + sed -ri 's/(^authorizer:).*/\1 CassandraAuthorizer/' "$CASSANDRA_CONF_DIR$CASSANDRA_CONF_FILE" +# echo config changed +} + +# turn on the JMX authentication using Cassandra's internal authentication and authorization +function turn_on_jmx_authentication() { + # disable JMX local + JMX_LOCAL=no + echo jmx_local: $JMX_LOCAL + +# so far this is not working because cassandra-env.sh file is not modifiable (sits in scripts directory) + # update the config file cassandra-env.sh + sed -ri 's/^(.*jmxremote\.password)/#\1/' "/usr/share/cassandra/cassandra-env.sh" + sed -ri 's/^#(.*config=CassandraLogin.*$)/\1/' "/usr/share/cassandra/cassandra-env.sh" + sed -ri 's/^#(.*auth\.login.*$)/\1/' "/usr/share/cassandra/cassandra-env.sh" + sed -ri 's/^#(.*AuthorizationProxy.*$)/\1/' "/usr/share/cassandra/cassandra-env.sh" + echo config updated +} diff --git a/test/common b/test/common new file mode 100644 index 0000000..54cd9d1 --- /dev/null +++ b/test/common @@ -0,0 +1,109 @@ +# +# Test a container image. +# +# Always use sourced from a specific container testfile +# +# reguires definition of CID_FILE_DIR +# CID_FILE_DIR=$(mktemp --suffix=_test_cidfiles -d) + +# may be redefined in the specific container testfile +EXPECTED_EXIT_CODE=0 + +function cleanup() { + for cid_file in $CID_FILE_DIR/* ; do + CONTAINER=$(cat $cid_file) + + : "Stopping and removing container $CONTAINER..." + docker stop $CONTAINER + exit_status=$(docker inspect -f '{{.State.ExitCode}}' $CONTAINER) + if [ "$exit_status" != "$EXPECTED_EXIT_CODE" ]; then + : "Dumping logs for $CONTAINER" + docker logs $CONTAINER + fi + docker rm $CONTAINER + rm $cid_file + done + rmdir $CID_FILE_DIR + : "Done." +} +trap cleanup EXIT SIGINT + +function get_cid() { + local name="$1" ; shift || return 1 + echo $(cat "$CID_FILE_DIR/$name") +} + +function get_container_ip() { + local id="$1" ; shift + docker inspect --format='{{.NetworkSettings.IPAddress}}' $(get_cid "$id") +} + +function wait_for_cid() { + local max_attempts=10 + local sleep_time=1 + local attempt=1 + local result=1 + while [ $attempt -le $max_attempts ]; do + [ -f $cid_file ] && [ -s $cid_file ] && break + : "Waiting for container start..." + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done +} + +# Make sure the invocation of docker run fails. +function assert_container_creation_fails() { + + # Time the docker run command. It should fail. If it doesn't fail, + # container will keep running so we kill it with SIGKILL to make sure + # timeout returns a non-zero value. + set +e + timeout -s SIGTERM --preserve-status 10s docker run --rm "$@" $IMAGE_NAME + ret=$? + set -e + + # Timeout will exit with a high number. + if [ $ret -gt 128 ]; then + return 1 + fi +} + +# to pass some arguments you need to specify CONTAINER_ARGS variable +function create_container() { + cid_file="$CID_FILE_DIR/$1" ; shift + # create container with a cidfile in a directory for cleanup + docker run ${CONTAINER_ARGS:-} --cidfile="$cid_file" -d $IMAGE_NAME "$@" + : "Created container $(cat $cid_file)" + wait_for_cid +} + +function run_doc_test() { + local tmpdir=$(mktemp -d) + local f + : " Testing documentation in the container image" + # Extract the help files from the container + for f in help.1 ; do + docker run --rm ${IMAGE_NAME} /bin/bash -c "cat /${f}" >${tmpdir}/$(basename ${f}) + # Check whether the files include some important information + for term in $@ ; do + if ! cat ${tmpdir}/$(basename ${f}) | grep -F -q -e "${term}" ; then + echo "ERROR: File /${f} does not include '${term}'." + return 1 + fi + done + done + # Check whether the files use the correct format + if ! file ${tmpdir}/help.1 | grep -q roff ; then + echo "ERROR: /help.1 is not in troff or groff format" + return 1 + fi + : " Success!" +} + +function run_all_tests() { + for test_case in $TEST_LIST; do + : "Running test $test_case" + $test_case + done; +} + diff --git a/test/run b/test/run new file mode 100755 index 0000000..0067ce5 --- /dev/null +++ b/test/run @@ -0,0 +1,207 @@ +#!/bin/bash +# +# Test the Cassandra image. +# +# IMAGE_NAME specifies the name of the candidate image used for testing. +# The image has to be available before this script is executed. +# + +set -exo nounset +shopt -s nullglob + +IMAGE_NAME=${IMAGE_NAME-mycass} + +source test/common + +TEST_LIST="\ +run_container_creation_tests +run_configuration_tests +run_general_tests +run_mount_config_test" +# the change password tests does not work or make sense in cassandra +#run_change_password_test +# the doc test is not working yet +#run_doc_test CASSANDRA_ADMIN_PASSWORD volume 9042" + +test $# -eq 1 -a "${1-}" == --list && echo "$TEST_LIST" && exit 0 +test -n "${IMAGE_NAME-}" || false 'make sure $IMAGE_NAME is defined' + +CID_FILE_DIR=$(mktemp --suffix=cassandra_test_cidfiles -d) + +# used in cleanup function +EXPECTED_EXIT_CODE=143 + +function cqlsh_cmd() { + docker run --rm "$IMAGE_NAME" cqlsh "$@" +} + +function remove_container() { + local name="$1" ; shift + CONTAINER=$(get_cid ${name}) + : "Stopping and removing container $CONTAINER..." + docker exec $CONTAINER nodetool stopdaemon || [ "$?" == "137" ] + while [ "$(docker inspect -f '{{.State.Running}}' $CONTAINER)" == "true" ] ; do + sleep 2 + done + exit_status=$(docker inspect -f '{{.State.ExitCode}}' $CONTAINER) + if [ "$exit_status" != "0" ]; then + : "Dumping logs for $CONTAINER" + docker logs $CONTAINER | tail + fi + docker rm $CONTAINER + rm $CID_FILE_DIR/$name + : "Removed." +} + +function test_config_option() { + local setting=$1 ; shift + local value=$1 ; shift + local name="configuration_${setting}" + + CONTAINER_ARGS=" +-e CASSANDRA_${setting^^}=${value} +" + create_container $name + + test_connection $name + + # If nothing is found, grep returns 1 and test fails. + docker exec $(get_cid ${name}) bash -c "cat /etc/cassandra/cassandra.yaml | grep -q ${setting}:\ ${value}" + + remove_container $name +} + +function test_connection() { + local name="$1" ; shift + local ip=$(get_container_ip $name) + : " Testing cqlsh connection to $ip..." + local max_attempts=20 + local sleep_time=2 + local i + for i in $(seq $max_attempts); do + : " Trying to connect..." + if cqlsh_cmd "$ip" "$@" <<< 'exit'; then + : " Success!" + return 0 + fi + sleep $sleep_time + done + echo "ERROR: Giving up, failed to connect." + return 1 +} + +function test_general() { + : " Testing general usage ('$1')" + local name=$1 ; shift + + create_container $name + + test_connection $name + + test_cqlsh $name + + remove_container $name +} + +function test_cqlsh() { + local name="$1" ; shift + local ip=$(get_container_ip $name) + : " Testing basic cqlsh commands" + cqlsh_cmd "$ip" <<< 'CREATE KEYSPACE cycling WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };' + cqlsh_cmd "$ip" <<< 'CREATE TABLE cycling.cyclist_name ( id UUID, fname text, lname text, PRIMARY KEY (id));' + cqlsh_cmd "$ip" <<< 'INSERT INTO cycling.cyclist_name (id, fname, lname) VALUES (7562c0f3-2f6c-41da-b276-88abac471eaf, 'john', 'smith');' + cqlsh_cmd "$ip" <<< 'INSERT INTO cycling.cyclist_name (id, fname, lname) VALUES (e69be414-f7eb-4e5a-b635-446ee5849810, 'john', 'doe');' + cqlsh_cmd "$ip" <<< 'INSERT INTO cycling.cyclist_name (id, fname, lname) VALUES (d4aa012a-8d71-4189-bcc8-24a858b713b6, 'john', 'smith');' + cqlsh_cmd "$ip" <<< 'SELECT * FROM cycling.cyclist_name WHERE lname = 'smith' ALLOW FILTERING;' + cqlsh_cmd "$ip" <<< 'DROP TABLE cycling.cyclist_name;' + cqlsh_cmd "$ip" <<< 'DROP KEYSPACE cycling;' + : " Success!" +} + +function run_container_creation_tests() { + +# there are no invalid combinations of variables in cassandra yet +# : " Testing invalid combinations of variables" +# assert_container_creation_fails +# : " Success!" + + : " Testing invalid values of variables" + assert_container_creation_fails -e CASSANDRA_CLUSTER_NAME=cool cluster + : " Success!" +} + +function run_configuration_tests() { + : " Testing image configuration settings" + test_config_option num_tokens 256 + # not allowing to have a space character in the cluster name + test_config_option cluster_name cool_cluster + : " Success!" +} + +function run_general_tests() { + CONTAINER_ARGS= test_general no_admin + # Test with arbitrary uid for the container + # the permissions are not set for arbitrary user to run the container + #CONTAINER_ARGS="-u 12345" run_tests no_admin_altuid +} + +function run_mount_config_test() { + local name="mount_config" + : " Testing config file mount" + local tmpdir=$(mktemp -d) + chmod a+rwx $tmpdir + config_file=${tmpdir}/cassandra.yaml + echo 'cluster_name: cool_cluster +commitlog_sync: periodic +commitlog_sync_period_in_ms: 10000 +partitioner: org.apache.cassandra.dht.Murmur3Partitioner +endpoint_snitch: SimpleSnitch +start_native_transport: true +seed_provider: + - class_name: org.apache.cassandra.locator.SimpleSeedProvider + parameters: + - seeds: "172.17.0.2"' > $config_file + chmod a+r ${config_file} + CONTAINER_ARGS=" +-v ${config_file}:/etc/cassandra/cassandra.yaml:Z +" + create_container $name + # need this to wait for the container to start up + test_connection $name + : " Testing if mounted config file works" + docker exec $(get_cid ${name}) nodetool describecluster | grep -q Name:\ cool_cluster + rm -r $tmpdir + : " Success!" +} + +function run_change_password_test() { + local name="change_password" + local admin_password='adminPassword' + local volume_dir + volume_dir=`mktemp -d --tmpdir cassandra-testdata.XXXXX` + chmod a+rwx ${volume_dir} + CONTAINER_ARGS=" +-e CASSANDRA_ADMIN_PASSWORD=${admin_password} +-v ${volume_dir}:/var/lib/cassandra/data:Z +" + create_container $name + # need this to wait for the container to start up + test_connection $name -u admin -p ${admin_password} + + echo " Changing passwords" + docker stop $(get_cid ${name}) + CONTAINER_ARGS=" +-e CASSANDRA_ADMIN_PASSWORD=NEW_${admin_password} +-v ${volume_dir}:/var/lib/cassandra/data:Z +" + create_container "${name}_NEW" + # need this to wait for the container to start up + test_connection "${name}_NEW" -u admin -p NEW_${admin_password} + # need to remove volume_dir with sudo because of permissions of files written + # by the Docker container + sudo rm -rf ${volume_dir} + echo " Success!" +} + +# Run the chosen tests +TEST_LIST=${@:-$TEST_LIST} run_all_tests