diff --git a/.travis.yml b/.travis.yml index e9cb4dca1..f6912e94b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,8 @@ go: - 1.7 - 1.8.3 +go_import_path: github.com/kubeless/kubeless + services: - docker env: @@ -27,6 +29,8 @@ install: - chmod +x $GOPATH/bin/kubecfg - git clone --depth=1 https://github.com/ksonnet/ksonnet-lib.git - export KUBECFG_JPATH=$PWD/ksonnet-lib + - git clone --depth=1 https://github.com/sstephenson/bats.git bats + - export PATH=$PATH:$PWD/bats/bin script: - make test @@ -37,6 +41,10 @@ script: make controller-image CONTROLLER_IMAGE=$CONTROLLER_IMAGE fi - make all-yaml + - | + if [ "$TRAVIS_OS_NAME" = linux ]; then + make integration-tests + fi after_success: - | diff --git a/Makefile b/Makefile index f099425ef..a366c5022 100644 --- a/Makefile +++ b/Makefile @@ -60,8 +60,11 @@ validation: ./script/validate-gofmt ./script/validate-git-marks +integration-tests: + ./script/integration-tests + minikube-rbac-test: - ./script/minikube-rbac-test + ./script/integration-test-rbac minikube fmt: $(GOFMT) -s -w $(GO_FILES) diff --git a/examples/Makefile b/examples/Makefile index 6ba610622..f36be35ab 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -2,6 +2,9 @@ get-python: kubeless function deploy get-python --trigger-http --runtime python2.7 --handler helloget.foo --from-file python/helloget.py echo "curl localhost:8080/api/v1/proxy/namespaces/default/services/get-python/" +get-python-verify: + kubeless function call get-python |egrep hello.world + get-nodejs: kubeless function deploy get-nodejs --trigger-http --runtime nodejs6 --handler helloget.foo --from-file nodejs/helloget.js echo "curl localhost:8080/api/v1/proxy/namespaces/default/services/get-nodejs/" @@ -12,14 +15,23 @@ get-python-metadata: get: get-python get-nodejs get-python-metadata +get-nodejs-verify: + kubeless function call get-nodejs |egrep hello.world + post-python: kubeless function deploy post-python --trigger-http --runtime python2.7 --handler hellowithdata.handler --from-file python/hellowithdata.py echo "curl --data '{\"hello\":\"world\"}' localhost:8080/api/v1/proxy/namespaces/default/services/post-python/ --header \"Content-Type:application/json\"" +post-python-verify: + kubeless function call post-python --data '{"it-s": "alive"}'|egrep "it.*alive" + post-nodejs: kubeless function deploy post-nodejs --trigger-http --runtime nodejs6 --handler hellowithdata.handler --from-file nodejs/hellowithdata.js echo "curl --data '{\"hello\":\"world\"}' localhost:8080/api/v1/proxy/namespaces/default/services/post-nodejs/ --header \"Content-Type:application/json\"" +post-nodejs-verify: + kubeless function call post-nodejs --data '{"it-s": "alive"}'|egrep "it.*alive" + post: post-python post-nodejs pubsub: diff --git a/kubeless-rbac-novols.jsonnet b/kubeless-rbac-novols.jsonnet new file mode 100644 index 000000000..68a864b24 --- /dev/null +++ b/kubeless-rbac-novols.jsonnet @@ -0,0 +1,9 @@ +# Remove volumeClaimTemplates from kafkaSts to enable testing kubeless +# on simple clusters deploys like kubeadm-dind-cluster +local kubeless_rbac = import "kubeless-rbac.jsonnet"; + +kubeless_rbac + { + kafkaSts+: + {spec+: {volumeClaimTemplates: []}} + + {spec+: {template+: {spec+: {volumes: [{name: "datadir", emptyDir: {}}]}}}} +} diff --git a/script/binary b/script/binary index faab7b2c2..69dad43fb 100755 --- a/script/binary +++ b/script/binary @@ -17,7 +17,7 @@ set -e if [ -z "$1" ]; then - VERSION=dev-$(shell date +%FT%T%z) + VERSION=dev-$(date +%FT%T%z) else VERSION=$1 fi diff --git a/script/integration-tests b/script/integration-tests new file mode 100755 index 000000000..f54cbe137 --- /dev/null +++ b/script/integration-tests @@ -0,0 +1,44 @@ +#!/bin/bash +# Special case: if ./ksonnet-lib exists, set KUBECFG_JPATH +test -d $PWD/ksonnet-lib && export KUBECFG_JPATH=$PWD/ksonnet-lib + +# We require below env +: ${GOPATH:?} ${KUBECFG_JPATH:?} + +# Default to "dind" kubernetes context +INTEGRATION_TESTS_CTX=${1:-dind} + +# ... and 'bats' installed +which bats > /dev/null || { + echo "ERROR: 'bats' is required to run these tests," \ + "install it from https://github.com/sstephenson/bats" + exit 255 +} + +k8s_create_dind() { + # Bring up kubeadm-dind-cluster (docker-in-docker k8s cluster) + DIND_CLUSTER_SH=dind-cluster-v1.7.sh + DIND_URL=https://cdn.rawgit.com/Mirantis/kubeadm-dind-cluster/master/fixed/${DIND_CLUSTER_SH} + rm -f ${DIND_CLUSTER_SH} + wget ${DIND_URL} + chmod +x ${DIND_CLUSTER_SH} + ./${DIND_CLUSTER_SH} up + export PATH="$HOME/.kubeadm-dind-cluster:$PATH" + sleep 5 +} + +## main() ## +# Create k8s cluster (only "dind" supported atm) if missing: +kubectl get nodes --context=${INTEGRATION_TESTS_CTX:?} || k8s_create_${INTEGRATION_TESTS_CTX} || exit 255 +export TEST_CONTEXT=${INTEGRATION_TESTS_CTX} + +source script/libtest.bash +trap k8s_context_restore 0 +k8s_context_save + +# Run the tests thru bats: +kubectl create namespace kubeless +bats tests/integration-tests.bats + +# Just showing remaining k8s objects +kubectl get all --all-namespaces diff --git a/script/libtest.bash b/script/libtest.bash new file mode 100644 index 000000000..2b10c47ad --- /dev/null +++ b/script/libtest.bash @@ -0,0 +1,185 @@ +#!/bin/bash +# k8s and kubeless helpers, specially "wait"-ers on pod ready/deleted/etc + +KUBELESS_JSONNET=kubeless.jsonnet +KUBELESS_JSONNET_RBAC=kubeless-rbac-novols.jsonnet + +KUBECTL_BIN=$(which kubectl) +KUBECFG_BIN=$(which kubecfg) + +export TEST_MAX_WAIT_SEC=120 + +# Workaround 'bats' lack of forced output support, dup() stderr fd +exec 9>&2 +echo_info() { + test -z "$TEST_DEBUG" && return 0 + echo "INFO: $*" >&9 +} +export -f echo_info + +kubectl() { + ${KUBECTL_BIN:?} --context=${TEST_CONTEXT:?} "$@" +} +kubecfg() { + ${KUBECFG_BIN:?} --context=${TEST_CONTEXT:?} "$@" +} + +## k8s specific Helper functions +k8s_wait_for_pod_ready() { + echo_info "Waiting for pod '${@}' to be ready ... " + local -i cnt=${TEST_MAX_WAIT_SEC:?} + until kubectl get pod "${@}" |&grep -q Running; do + ((cnt=cnt-1)) || return 1 + sleep 1 + done +} +k8s_wait_for_pod_gone() { + echo_info "Waiting for pod '${@}' to be gone ... " + local -i cnt=${TEST_MAX_WAIT_SEC:?} + until kubectl get pod "${@}" |&grep -q No.resources.found; do + ((cnt=cnt-1)) || return 1 + sleep 1 + done +} +k8s_wait_for_pod_logline() { + local string="${1:?}"; shift + local -i cnt=${TEST_MAX_WAIT_SEC:?} + echo_info "Waiting for '${@}' to show logline '${string}' ..." + until kubectl logs --tail=10 "${@}"|&grep -q "${string}"; do + ((cnt=cnt-1)) || return 1 + sleep 1 + done +} +k8s_context_save() { + TEST_CONTEXT_SAVED=$(${KUBECTL_BIN} config current-context) + # Kubeless doesn't support contexts yet, save+restore it + # Don't save current_context if it's the same already + [[ $TEST_CONTEXT_SAVED == $TEST_CONTEXT ]] && TEST_CONTEXT_SAVED="" + + # Save current_context + [[ $TEST_CONTEXT_SAVED != "" ]] && \ + echo_info "Saved context: '${TEST_CONTEXT_SAVED}'" && \ + ${KUBECTL_BIN} config use-context ${TEST_CONTEXT} +} +k8s_context_restore() { + # Restore saved context + [[ $TEST_CONTEXT_SAVED != "" ]] && \ + echo_info "Restoring context: '${TEST_CONTEXT_SAVED}'" && \ + ${KUBECTL_BIN} config use-context ${TEST_CONTEXT_SAVED} +} +_wait_for_cmd_ok() { + local cmd="${*:?}"; shift + local -i cnt=${TEST_MAX_WAIT_SEC:?} + echo_info "Waiting for '${*}' to successfully exit ..." + until env ${cmd}; do + ((cnt=cnt-1)) || return 1 + sleep 1 + done +} + +## Specific for kubeless +kubeless_recreate() { + local jsonnet_del=${1:?missing jsonnet delete manifest} jsonnet_upd=${2:?missing jsonnet update manifest} + local -i cnt=${TEST_MAX_WAIT_SEC:?} + echo_info "Delete kubeless namespace, wait to be gone ... " + kubecfg delete ${jsonnet_del} + kubectl delete namespace kubeless >& /dev/null || true + while kubectl get namespace kubeless >& /dev/null; do + ((cnt=cnt-1)) || return 1 + sleep 1 + done + kubectl create namespace kubeless + kubecfg update ${jsonnet_upd} +} +kubeless_function_delete() { + local func=${1:?}; shift + echo_info "Deleting function "${func}" in case still present ... " + kubeless function delete "${func}" >& /dev/null || true + kubectl delete all -l function="${func}" > /dev/null || true + k8s_wait_for_pod_gone -l function="${func}" +} +kubeless_function_deploy() { + local func=${1:?}; shift + echo_info "Deploying function ..." + kubeless function deploy ${func} ${@} +} +kubeless_function_exp_regex_call() { + local exp_rc=${1:?} regex=${2:?} func=${3:?}; shift 3 + echo_info "Calling function ${func}, expecting rc=${exp_rc} " + kubeless function call ${func} "${@}"|&egrep ${regex} +} +_wait_for_kubeless_controller_ready() { + echo_info "Waiting for kubeless controller to be ready ... " + k8s_wait_for_pod_ready -n kubeless -l kubeless=controller + _wait_for_cmd_ok kubectl get functions 2>/dev/null +} +_wait_for_controller_logline() { + local string="${1:?}" + k8s_wait_for_pod_logline "${string}" -n kubeless -l kubeless=controller +} +_wait_for_simple_function_pod_ready() { + k8s_wait_for_pod_ready -l function=get-python +} +_deploy_simple_function() { + make -C examples get-python +} +_call_simple_function() { + # Artifact to dodge 'bats' lack of support for positively testing _for_ errors + case "${1:?}" in + 1) make -C examples get-python-verify |& egrep Error.1;; + 0) make -C examples get-python-verify;; + esac +} +_delete_simple_function() { + kubeless_function_delete get-python +} + +## Entry points used by 'bats' tests: +verify_k8s_tools() { + local tools="kubectl kubecfg kubeless" + for exe in $tools; do + which ${exe} >/dev/null && continue + echo "ERROR: '${exe}' needs to be installed" + return 1 + done +} +verify_minikube_running () { + [[ $TEST_CONTEXT == minikube ]] || return 0 + minikube status | grep -q "minikube: Running" && return 0 + echo "ERROR: minikube not running." + return 1 +} +verify_rbac_mode() { + kubectl api-versions |&grep -q rbac && return 0 + echo "ERROR: Please run w/RBAC, eg minikube as: minikube start --extra-config=apiserver.Authorization.Mode=RBAC" + return 1 +} +test_must_fail_without_rbac_roles() { + echo_info "RBAC TEST: function deploy/call must fail without RBAC roles" + _delete_simple_function + kubeless_recreate $KUBELESS_JSONNET_RBAC $KUBELESS_JSONNET + _wait_for_kubeless_controller_ready + _deploy_simple_function + _wait_for_controller_logline "User.*cannot" + _call_simple_function 1 +} +test_must_pass_with_rbac_roles() { + echo_info "RBAC TEST: function deploy/call must succeed with RBAC roles" + _delete_simple_function + kubeless_recreate $KUBELESS_JSONNET_RBAC $KUBELESS_JSONNET_RBAC + _wait_for_kubeless_controller_ready + _deploy_simple_function + _wait_for_controller_logline "controller synced and ready" + _wait_for_simple_function_pod_ready + _call_simple_function 0 +} + +test_kubeless_function() { + local func=${1:?} + echo_info "TEST: $func" + kubeless_function_delete ${func} + make -sC examples ${func} + k8s_wait_for_pod_ready -l function=${func} + make -sC examples ${func}-verify +} +# vim: sw=4 ts=4 et si diff --git a/script/minikube-rbac-test b/script/minikube-rbac-test deleted file mode 100755 index c1b785031..000000000 --- a/script/minikube-rbac-test +++ /dev/null @@ -1,158 +0,0 @@ -#!/bin/bash -# Instrument basic smoke RBAC test against minikube: -# - _test_must_fail_without_rbac_roles for failure -# - _test_must_pass_with_rbac_roles for success -# -# Assumes already setup minikube RBAC'd environment -set -u - -KUBECTL_BIN=$(which kubectl) -KUBECFG_BIN=$(which kubecfg) -MINIKUBE_CONTEXT="minikube" - -T_BOLD=$(test -t 0 && tput bold) -T_SGR0=$(test -t 0 && tput sgr0) - -typeset -i TOTAL_PASS=0 -typeset -i TOTAL_FAIL=0 - -# Wrapup kubectl, kubecfg for --context -kubectl() { - ${KUBECTL_BIN} --context=${MINIKUBE_CONTEXT} "$@" -} -kubecfg() { - ${KUBECFG_BIN} --context=${MINIKUBE_CONTEXT} "$@" -} - -## Generic helper functions -info () { - echo "INFO: $@" -} -spin() { - echo -n .; sleep $1; echo -ne "\r"; sleep $1 -} - -_pass_or_fail() { - local rc=${1:?} exp_rc=${2:?} msg="${3:?}" status - [[ ${rc} == ${exp_rc} ]] \ - && { status=PASS; TOTAL_PASS=TOTAL_PASS+1; } \ - || { status=FAIL; TOTAL_FAIL=TOTAL_FAIL+1; } - echo "${T_BOLD}${status}${T_SGR0}: ${msg}" - [[ ${status} = PASS ]] -} - -## Pre-run verifications -verify_k8s_tools() { - local tools="minikube kubectl kubecfg kubeless" - info "VERIFY: k8s tools installed: $tools" - for exe in minikube kubectl kubecfg kubeless; do - which ${exe} >/dev/null && continue - echo "ERROR: '${exe}' needs to be installed" - return 1 - done -} - -verify_minikube_running () { - info "VERIFY: minikube running ..." - minikube status | grep -q "minikube: Running" -} -verify_minikube_rbac_mode() { - info "VERIFY: minikube running with RBAC ... " - kubectl api-versions |&grep -q rbac && return 0 - echo "ERROR: Please run minikube as: minikube start --extra-config=apiserver.Authorization.Mode=RBAC" - return 1 -} - -## k8s specific Helper functions -_wait_for_kubeless_controller_ready() { - info "Waiting for kubeless controller to be ready ... " - until kubectl get pod --namespace=kubeless --selector=kubeless=controller|&egrep -q Running; do - spin 0.5 - done - sleep 10 -} -_recreate_kubeless() { - local jsonnet_del=${1:?missing jsonnet delete manifest} jsonnet_upd=${2:?missing jsonnet update manifest} - info "Delete kubeless namespace, wait to be gone ... " - kubecfg delete ${jsonnet_del} - kubectl delete namespace kubeless >& /dev/null || true - while kubectl get namespace kubeless >& /dev/null; do - spin 0.5 - done - kubectl create namespace kubeless - kubecfg update ${jsonnet_upd} -} -_wait_for_function_pod_ready() { - info "Waiting for function pod to be ready ... " - until kubectl get pod --selector=function=get-python |&grep -q Running; do - spin 0.5 - done -} -_wait_for_controller_logline() { - local string="${1:?}" - info "Waiting for controller to show logline '${1}' ..." - until kubectl --context=minikube logs --tail=10 --namespace=kubeless --selector=kubeless=controller|&grep -q "${string}"; do - spin 0.5 - done - _pass_or_fail $? 0 "Found logline: '$string'" -} -_delete_function() { - info "Deleting function in case still present ... " - kubeless function delete get-python >& /dev/null || true - kubectl delete all --selector=function=get-python >& /dev/null || true -} -_deploy_function() { - info "Deploying function ..." - kubeless function deploy get-python --runtime python27 --handler hellowithdata.handler --from-file examples/python/hellowithdata.py --trigger-http -} -_call_function() { - local exp_rc=${1:-0} - info "Calling function, expecting rc=${exp_rc} " - kubeless function call get-python --data '{"it-s": "alive"}' |&egrep it.*alive - _pass_or_fail $? ${exp_rc} "called function, got rc=$?" -} -_test_must_fail_without_rbac_roles() { - info "RBAC TEST: function deploy/call must fail without RBAC roles" - _delete_function - _recreate_kubeless kubeless-rbac.jsonnet kubeless.jsonnet - _wait_for_kubeless_controller_ready - _deploy_function - _wait_for_controller_logline "User.*cannot" - _call_function 1 -} -_test_must_pass_with_rbac_roles() { - info "RBAC TEST: function deploy/call must succeed with RBAC roles" - _delete_function - _recreate_kubeless kubeless-rbac.jsonnet kubeless-rbac.jsonnet - _wait_for_kubeless_controller_ready - _deploy_function - _wait_for_controller_logline "controller synced and ready" - _wait_for_function_pod_ready - _call_function 0 -} -test_kubeless_rbac() { - local current_context=$(${KUBECTL_BIN} config current-context) - # Kubeless doesn't support contexts yet, save+restore it - # Don't save current_context if it's "minikube" already - [[ $current_context == $MINIKUBE_CONTEXT ]] && current_context="" - - # Save current_context - [[ $current_context != "" ]] && \ - info "Saved context: '${current_context}'" && \ - ${KUBECTL_BIN} config use-context ${MINIKUBE_CONTEXT} - _test_must_fail_without_rbac_roles - _test_must_pass_with_rbac_roles - # Restore current_context - [[ $current_context != "" ]] && \ - info "Restoring context: '${current_context}'" && \ - ${KUBECTL_BIN} config use-context ${current_context} -} - -verify_k8s_tools || exit 255 -verify_minikube_running || exit 255 -verify_minikube_rbac_mode || exit 255 -test_kubeless_rbac -info "exit ${T_BOLD}PASS=$TOTAL_PASS FAIL=$TOTAL_FAIL${T_SGR0}" -exit $TOTAL_FAIL - -# vim: sw=4 ts=4 et si diff --git a/tests/integration-tests.bats b/tests/integration-tests.bats new file mode 100644 index 000000000..cb3c12f66 --- /dev/null +++ b/tests/integration-tests.bats @@ -0,0 +1,37 @@ +#!/usr/bin/env bats + +load ../script/libtest + +@test "Verify TEST_CONTEXT envvar" { + : ${TEST_CONTEXT:?} +} +@test "Verify needed kubernetes tools installed" { + verify_k8s_tools +} +@test "Verify minikube running (if context=='minikube')" { + verify_minikube_running +} +@test "Verify k8s RBAC mode" { + verify_rbac_mode +} +@test "Test simple function failure without RBAC rules" { + test_must_fail_without_rbac_roles +} +@test "Test simple function success with proper RBAC rules" { + test_must_pass_with_rbac_roles +} +# 'bats' lacks loop support, unroll-them-all -> +@test "Test function: get-python" { + test_kubeless_function get-python +} +@test "Test function: get-nodejs" { + test_kubeless_function get-nodejs +} +@test "Test function: post-python" { + test_kubeless_function post-python +} +@test "Test function: post-nodejs" { + test_kubeless_function post-nodejs +} + +# vim: ts=2 sw=2 si et syntax=sh