Skip to content

Commit

Permalink
ci: add e2e test scripts
Browse files Browse the repository at this point in the history
Signed-off-by: Phoeniix Zhao <[email protected]>
  • Loading branch information
Phoenix500526 committed Nov 27, 2023
1 parent e1d1d03 commit e0178cf
Show file tree
Hide file tree
Showing 13 changed files with 483 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .github/scripts/install_deps.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
apt-get install -y make expect libssl-dev

# install minikube
curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
install -m 755 minikube /usr/local/bin/minikube
32 changes: 32 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: E2E

on:
pull_request:
branches:
- main
workflow_dispatch: { }

# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency
concurrency:
group: ${{ github.workflow }}-${{ github.actor }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true


jobs:
validation:
name: 'Validation'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Dependencies
run: sudo bash ./.github/scripts/install_deps.sh
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version-file: go/src/github.com/${{ github.repository }}/go.mod
go-version: 1.21.3
- name: 'E2E CI'
run: bash ./tests/e2e/e2e.sh -p ci
- name: clean
if: failure()
run: bash ./tests/e2e/e2e.sh -c
3 changes: 3 additions & 0 deletions test/e2e/cases/cases.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
${__E2E_CASES__:=false} && return 0 || __E2E_CASES__=true

source "$(dirname "${BASH_SOURCE[0]}")/ci.sh"
126 changes: 126 additions & 0 deletions test/e2e/cases/ci.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
${__E2E_CASES_CI__:=false} && return 0 || __E2E_CASES_CI__=true

source "$(dirname "${BASH_SOURCE[0]}")/../common/common.sh"
source "$(dirname "${BASH_SOURCE[0]}")/../testenv/testenv.sh"

_TEST_CI_CLUSTER_NAME="my-xline-cluster"
_TEST_CI_STS_NAME="$_TEST_CI_CLUSTER_NAME-sts"
_TEST_CI_SVC_NAME="$_TEST_CI_CLUSTER_NAME-svc"
_TEST_CI_NAMESPACE="default"
_TEST_CI_DNS_SUFFIX="svc.cluster.local"
_TEST_CI_XLINE_PORT="2379"
_TEST_CI_LOG_SYNC_TIMEOUT=60

function test::ci::_mk_endpoints() {
local endpoints="${_TEST_CI_STS_NAME}-0.${_TEST_CI_SVC_NAME}.${_TEST_CI_NAMESPACE}.${_TEST_CI_DNS_SUFFIX}:${_TEST_CI_XLINE_PORT}"
for ((i = 1; i < $1; i++)); do
endpoints="${endpoints},${_TEST_CI_STS_NAME}-${i}.${_TEST_CI_SVC_NAME}.${_TEST_CI_NAMESPACE}.${_TEST_CI_DNS_SUFFIX}:${_TEST_CI_XLINE_PORT}"
done
echo "$endpoints"
}

function test::ci::_etcdctl_expect() {
log::debug "run command: etcdctl --endpoints=$1 $2"
got=$(testenv::util::etcdctl --endpoints="$1" "$2")
expect=$(echo -e "$3")
if [ "${got//$'\r'/}" == "$expect" ]; then
log::info "command run success"
else
log::error "command run failed"
log::error "expect: $expect"
log::error "got: $got"
return 1
fi
}

function test::ci::_install_CRD() {
KUBECTL="minikube kubectl --" make install
if [ $? -eq 0 ]; then
log::info "make install: create custom resource definition succeeded"
else
log::error "make install: create custom resource definition failed"
fi
}

function test::ci::_uninstall_CRD() {
KUBECTL="minikube kubectl --" make uninstall
if [ $? -eq 0 ]; then
log::info "make uninstall: remove custom resource definition succeeded"
else
log::error "make uninstall: remove custom resource definition failed"
fi
}

function test::ci::wait_all_xline_pod_ready() {
for ((i = 0; i < $1; i++)); do
log::info "wait pod/${_TEST_CI_STS_NAME}-${i} to be ready"
if ! k8s::kubectl wait --for=condition=Ready pod/${_TEST_CI_STS_NAME}-${i} --timeout=300s; then
log::fatal "Failed to wait for util to be ready"
fi
done
}

function test::ci::_start() {
log::info "starting controller"
pushd $(dirname "${BASH_SOURCE[0]}")/../../../
test::ci::_install_CRD
KUBECTL="minikube kubectl --" make run >/dev/null 2>&1 &
log::info "controller started"
popd
log::info "starting xline cluster"
k8s::kubectl apply -f "$(dirname "${BASH_SOURCE[0]}")/manifests/cluster.yml" >/dev/null 2>&1
k8s::kubectl::wait_resource_creation sts $_TEST_CI_STS_NAME
}

function test::ci::_teardown() {
log::info "stopping controller"
pushd $(dirname "${BASH_SOURCE[0]}")/../../../
test::ci::_uninstall_CRD
controller_pid=$(ps aux | grep "[g]o run ./cmd/main.go" | awk '{print $2}')
if [ -n "$controller_pid" ]; then
kill -9 $controller_pid
fi
}

function test::ci::_chaos() {
size=$1
iters=$2
max_kill=$((size / 2))
log::info "chaos: size=$size, iters=$iters, max_kill=$max_kill"
for ((i = 0; i < iters; i++)); do
log::info "chaos: iter=$i"
endpoints=$(test::ci::_mk_endpoints size)
test::ci::_etcdctl_expect "$endpoints" "put A $i" "OK" || return $?
test::ci::_etcdctl_expect "$endpoints" "get A" "A\n$i" || return $?
kill=$((RANDOM % max_kill + 1))
log::info "chaos: kill=$kill"
for ((j = 0; j < kill; j++)); do
pod="${_TEST_CI_STS_NAME}-$((RANDOM % size))"
log::info "chaos: kill pod=$pod"
k8s::kubectl delete pod "$pod" --force --grace-period=0 2>/dev/null
done
test::ci::_etcdctl_expect "$endpoints" "put B $i" "OK" || return $?
test::ci::_etcdctl_expect "$endpoints" "get B" "B\n$i" || return $?
k8s::kubectl wait --for=jsonpath='{.status.readyReplicas}'="$size" sts/$_TEST_CI_CLUSTER_NAME --timeout=300s >/dev/null 2>&1
log::info "wait for log synchronization" && sleep $_TEST_CI_LOG_SYNC_TIMEOUT
done
}

function test::run::ci::basic_validation() {
test::ci::_start
test::ci::wait_all_xline_pod_ready 3
endpoints=$(test::ci::_mk_endpoints 3)
test::ci::_etcdctl_expect "$endpoints" "put A 1" "OK" || return $?
test::ci::_etcdctl_expect "$endpoints" "get A" "A\n1" || return $?
endpoints=$(test::ci::_mk_endpoints 1)
test::ci::_etcdctl_expect "$endpoints" "put A 2" "OK" || return $?
test::ci::_etcdctl_expect "$endpoints" "get A" "A\n2" || return $?
test::ci::_teardown
}


function test::run::ci::basic_chaos() {
test::ci::_start
test::ci::_chaos 3 5 || return $?
test::ci::_teardown
}
9 changes: 9 additions & 0 deletions test/e2e/cases/manifests/cluster.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: xline.kvstore.datenlord.com/v1alpha1
kind: XlineCluster
metadata:
name: my-xline-cluster
spec:
version: v0.6.0
image: phoenix500526/xline
imagePullPolicy: IfNotPresent
replicas: 3
4 changes: 4 additions & 0 deletions test/e2e/common/common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
${__E2E_COMMON__:=false} && return 0 || __E2E_COMMON__=true

source "$(dirname "${BASH_SOURCE[0]}")/log.sh"
source "$(dirname "${BASH_SOURCE[0]}")/k8s.sh"
45 changes: 45 additions & 0 deletions test/e2e/common/k8s.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
${__E2E_COMMON_K8S__:=false} && return 0 || __E2E_COMMON_K8S__=true

# ENVIRONMENT VARIABLES:
# KUBECTL: path to kubectl binary
# KUBECTL_NAMESPACE: namespace to use for kubectl commands
#
# ARGUMENTS:
# $@: arguments to pass to kubectl
function k8s::kubectl() {
KUBECTL_NAMESPACE="${KUBECTL_NAMESPACE:-default}"
local kubectl="${KUBECTL:-minikube kubectl --}"
${kubectl} -n "${KUBECTL_NAMESPACE}" "$@"
}

# ENVIRONMENT VARIABLES:
# KUBECTL: path to kubectl binary
# KUBECTL_NAMESPACE: namespace to use for kubectl commands
#
# ARGUMENTS:
# $1: resource type
# $2: resource name
function k8s::kubectl::resource_exist() {
KUBECTL_NAMESPACE="${KUBECTL_NAMESPACE:-default}"
k8s::kubectl get "$1" "$2" >/dev/null 2>&1
}

# ENVIRONMENT VARIABLES:
# KUBECTL: path to kubectl binary
# KUBECTL_NAMESPACE: namespace to use for kubectl commands
#
# ARGUMENTS:
# $1: resource type
# $2: resource name
# $3: (optional) interval to check resource creation
function k8s::kubectl::wait_resource_creation() {
interval="${3:-5}"
KUBECTL_NAMESPACE="${KUBECTL_NAMESPACE:-default}"
while true; do
if k8s::kubectl::resource_exist "$1" "$2"; then
break
fi
log::info "Waiting for $1/$2 ($KUBECTL_NAMESPACE) to be created"
sleep "$interval"
done
}
26 changes: 26 additions & 0 deletions test/e2e/common/log.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
${__E2E_COMMON_LOG__:=false} && return 0 || __E2E_COMMON_LOG__=true

# ENVIRONMENT VARIABLES
# E2E_DEBUG: If set to true, debug messages will be printed to stdout
function log::debug() {
if [[ "${E2E_DEBUG:=false}" == "true" ]]; then
echo -e "\033[00;34m" "[DEBUG]" "$@" "\033[0m"
fi
}

function log::info() {
echo -e "\033[00;32m" "[INFO]" "$@" "\033[0m"
}

function log::warn() {
echo -e "\033[00;33m" "[WARN]" "$@" "\033[0m"
}

function log::error() {
echo -e "\033[00;31m" "[ERROR]" "$@" "\033[0m"
}

function log::fatal() {
echo -e "\033[00;31m" "[FATAL]" "$@" "\033[0m"
exit 1
}
97 changes: 97 additions & 0 deletions test/e2e/e2e.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/bin/bash

set -euo pipefail

source "$(dirname "${BASH_SOURCE[0]}")/common/common.sh"
source "$(dirname "${BASH_SOURCE[0]}")/testenv/testenv.sh"
source "$(dirname "${BASH_SOURCE[0]}")/cases/cases.sh"

function setup() {
testenv::k8s::create
testenv::util::install
}

function teardown() {
testenv::k8s::delete
}

function list_test_cases() {
local -a functions
IFS=$'\n' read -d '' -ra functions <<<"$(compgen -A function | sort)" && unset IFS
local -a testcases=()
for func in "${functions[@]}"; do
if [[ "$func" =~ ^test::run:: ]]; then
testcase=${func#test::run::}
if [[ -n "${E2E_TEST_CASE_PREFIX:=}" && ${testcase} != "${E2E_TEST_CASE_PREFIX}"* ]]; then
continue
fi
testcases+=("$testcase")
fi
done
echo -n "${testcases[*]}"
}

function run() {
local -a testcases=()
IFS=" " read -ra testcases <<<"$(list_test_cases)" && unset IFS
local failed=0
local passed=0
for testcase in "${testcases[@]}"; do
log::info "=== Running test case: $testcase ==="
if test::run::"$testcase"; then
log::info "Test case passed: $testcase"
((passed++))
else
log::error "Test case failed: $testcase"
((failed++))
fi
done
if ((failed > 0)); then
log::error "Failed test cases: $failed/${#testcases[@]}"
return "${failed}"
else
log::info "All test cases passed"
fi
}

function help() {
echo "Xline Operator E2E Test Script"
echo ""
echo "Parameters:"
echo " -p <prefix> Run selected test cases with prefix"
echo " -h Print this help"
echo " -l List all test cases"
echo " -c Clean the kind cluster."
}

function main() {
while getopts "p:lhc" opt; do
case "$opt" in
p)
export E2E_TEST_CASE_PREFIX="$OPTARG"
log::info "Run selected test cases with prefix: $E2E_TEST_CASE_PREFIX"
;;
l)
for testcase in $(list_test_cases); do
echo "$testcase"
done
exit 0
;;
c)
teardown
exit 0
;;
h)
help
exit 0
;;
?) ;;
esac
done

setup || return $?
run || return $?
teardown
}

main "$@"
Loading

0 comments on commit e0178cf

Please sign in to comment.