今天在kubernetes中部署mongodb-rs时,发现其helm模板中使用到了一个bats,搜索了一下发现这是一个对bash领域的自动化测试框架,简单学习一下.
使用 mongodb-rs的部署不在这里详述了,直接来到使用到bats的地方,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 --- apiVersion: v1 kind: Pod metadata: labels: app: mongodb-replicaset chart: mongodb-replicaset-3.17.0 heritage: Helm release: mongodb-rs name: mongodb-rs-mongodb-replicaset-test namespace: infra annotations: "helm.sh/hook": test-success spec: initContainers: - name: test-framework image: dduportal/bats:0.4.0 command: - bash - -c - | set -ex # copy bats to tools dir cp -R /usr/local/libexec/ /tools/bats/ volumeMounts: - name: tools mountPath: /tools containers: - name: mongo image: "mongo:4.2" command: - /tools/bats/bats - -t - /tests/mongodb-up-test.sh env: - name: FULL_NAME value: mongodb-rs-mongodb-replicaset - name: NAMESPACE value: infra - name: REPLICAS value: "3" - name: AUTH value: "true" - name: ADMIN_USER valueFrom: secretKeyRef: name: "mongodb-rs-mongodb-replicaset-admin" key: user - name: ADMIN_PASSWORD valueFrom: secretKeyRef: name: "mongodb-rs-mongodb-replicaset-admin" key: password volumeMounts: - name: tools mountPath: /tools - name: tests mountPath: /tests volumes: - name: tools emptyDir: {} - name: tests configMap: name: mongodb-rs-mongodb-replicaset-tests restartPolicy: Never
首先看到initContainers执行了cp -R /usr/local/libexec/ /tools/bats/
,从bats github中可以看到,这个目录下是一些可执行的bash代码,这是整个核心.
然后在主containers中执行了/tools/bats/bats -t /tests/mongodb-up-test.sh
, 这个脚本内容如下:
cat mongodb-up-test.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 #!/usr/bin/env bash set -ex CACRT_FILE=/work-dir/tls.crt CAKEY_FILE=/work-dir/tls.key MONGOPEM=/work-dir/mongo.pem MONGOARGS="--quiet" if [ -e "/tls/tls.crt" ]; then mkdir -p /work-dir cp /tls/tls.crt /work-dir/tls.crt cp /tls/tls.key /work-dir/tls.key pushd /work-dir cat >openssl.cnf <<EOL [req] req_extensions = v3_req distinguished_name = req_distinguished_name [req_distinguished_name] [ v3_req ] basicConstraints = CA:FALSE keyUsage = nonRepudiation, digitalSignature, keyEncipherment subjectAltName = @alt_names [alt_names] DNS.1 = $(echo -n "$(hostname)" | sed s/-[0-9]*$//) DNS.2 = $(hostname) DNS.3 = localhost DNS.4 = 127.0.0.1 EOL openssl genrsa -out mongo.key 2048 openssl req -new -key mongo.key -out mongo.csr -subj "/OU=MongoDB/CN=$(hostname) " -config openssl.cnf openssl x509 -req -in mongo.csr \ -CA "$CACRT_FILE " -CAkey "$CAKEY_FILE " -CAcreateserial \ -out mongo.crt -days 3650 -extensions v3_req -extfile openssl.cnf cat mongo.crt mongo.key > $MONGOPEM MONGOARGS="$MONGOARGS --ssl --sslCAFile $CACRT_FILE --sslPEMKeyFile $MONGOPEM " fi if [[ "${AUTH} " == "true" ]]; then MONGOARGS="$MONGOARGS --username $ADMIN_USER --password $ADMIN_PASSWORD --authenticationDatabase admin" fi pod_name () { local full_name="${FULL_NAME?Environment variable FULL_NAME not set} " local namespace="${NAMESPACE?Environment variable NAMESPACE not set} " local index="$1 " echo "$full_name -$index .$full_name .$namespace .svc.cluster.local" } replicas () { echo "${REPLICAS?Environment variable REPLICAS not set} " } master_pod () { for ((i = 0; i < $(replicas); ++i)); do response=$(mongo $MONGOARGS "--host=$(pod_name "$i " ) " "--eval=rs.isMaster().ismaster" ) if [[ "$response " == "true" ]]; then pod_name "$i " break fi done } setup () { local ready=0 until [[ "$ready " -eq $(replicas) ]]; do echo "Waiting for application to become ready" >&2 sleep 1 ready=0 for ((i = 0; i < $(replicas); ++i)); do response=$(mongo $MONGOARGS "--host=$(pod_name "$i " ) " "--eval=rs.status().ok" || true ) if [[ "$response " -eq 1 ]]; then ready=$((ready + 1 )) fi done done } @test "Testing mongodb client is executable" { mongo -h [ "$?" -eq 0 ] } @test "Connect mongodb client to mongodb pods" { for ((i = 0; i < $(replicas); ++i)); do response=$(mongo $MONGOARGS "--host=$(pod_name "$i " ) " "--eval=rs.status().ok" ) if [[ ! "$response " -eq 1 ]]; then exit 1 fi done } @test "Write key to primary" { response=$(mongo $MONGOARGS --host=$(master_pod) "--eval=db.test.insert({\"abc\": \"def\"}).nInserted" ) if [[ ! "$response " -eq 1 ]]; then exit 1 fi } @test "Read key from slaves" { sleep 10 for ((i = 0; i < $(replicas); ++i)); do response=$(mongo $MONGOARGS --host=$(pod_name "$i " ) "--eval=rs.slaveOk(); db.test.find({\"abc\":\"def\"})" ) if [[ ! "$response " =~ .*def.* ]]; then exit 1 fi done mongo $MONGOARGS --host=$(master_pod) "--eval=db.test.deleteMany({\"abc\": \"def\"})" }
该脚本的上半部分可以先忽略
然后定义了几个函数, 其实setup()
函数在bats中是特殊的函数,其实还有一个teardown
,主要用于在执行测试函数前/后执行的两个函数, setup
用于做一些准备工作,teardown
用于做一些清理工作
最重要的部分是@test
,比如:
1 2 3 4 @test "Testing mongodb client is executable" { mongo -h [ "$?" -eq 0 ] }
@test
是一个关键字,指定被包含代码块需要被testing.在上面的脚本中指定了4个@test
函数. 它到底是如何执行的呢?
源码分析 首先: 在bats-preprocess 中,这个代码主要是解析待测试脚本,可以发现以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 pattern='^ *@test *([^ ].*) *\{ *(.*)$' while IFS= read -r line; do let index+=1 if [[ "$line " =~ $pattern ]]; then quoted_name="${BASH_REMATCH[1]} " body="${BASH_REMATCH[2]} " name="$(eval echo "$quoted_name " ) " encoded_name="$(encode_name "$name " ) " tests["${#tests[@]} " ]="$encoded_name " echo "${encoded_name} () { bats_test_begin ${quoted_name} ${index} ; ${body} " else printf "%s\n" "$line " fi done
通过解析@test
来得到有多个需要testing的代码块,从pod的日志中也可以看出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ++ bats_test_function test_Testing_mongodb_client_is_executable ++ local test_name=test_Testing_mongodb_client_is_executable ++ BATS_TEST_NAMES["${#BATS_TEST_NAMES[@]} " ]=test_Testing_mongodb_client_is_executable ++ bats_test_function test_Connect_mongodb_client_to_mongodb_pods ++ local test_name=test_Connect_mongodb_client_to_mongodb_pods ++ BATS_TEST_NAMES["${#BATS_TEST_NAMES[@]} " ]=test_Connect_mongodb_client_to_mongodb_pods ++ bats_test_function test_Write_key_to_primary ++ local test_name=test_Write_key_to_primary ++ BATS_TEST_NAMES["${#BATS_TEST_NAMES[@]} " ]=test_Write_key_to_primary ++ bats_test_function test_Read_key_from_slaves ++ local test_name=test_Read_key_from_slaves ++ BATS_TEST_NAMES["${#BATS_TEST_NAMES[@]} " ]=test_Read_key_from_slaves + '[' -n '' ']' + bats_perform_tests test_Testing_mongodb_client_is_executable test_Connect_mongodb_client_to_mongodb_pods test_Write_key_to_primary test_Read_key_from_slaves + echo 1..4 + test_number=1 + status=0 1..4 + for test_name in "$@ " + /tools/bats/bats-exec-test /tests/mongodb-up-test.sh test_Testing_mongodb_client_is_executable 1 +
从日志中可以看到,成功解析到了4个@test
代码块, 然后最后循环调用/tools/bats/bats-exec-test
, 来看看这个函数
传递了三个参数,第一个是脚本文件,第二个是获取到的@test
代码块的名字, 第三个是序号
到/tools/bats/bats-exec-test
,调用这个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 bats_perform_test () { BATS_TEST_NAME="$1 " if [ "$(type -t "$BATS_TEST_NAME " || true) " = "function" ]; then BATS_TEST_NUMBER="$2 " if [ -z "$BATS_TEST_NUMBER " ]; then echo "1..1" BATS_TEST_NUMBER="1" fi BATS_TEST_COMPLETED="" BATS_TEARDOWN_COMPLETED="" trap "bats_debug_trap \"\$BASH_SOURCE\"" debug trap "bats_error_trap" err trap "bats_teardown_trap" exit "$BATS_TEST_NAME " >>"$BATS_OUT " 2>&1 BATS_TEST_COMPLETED=1 else echo "bats: unknown test name \`$BATS_TEST_NAME '" >&2 exit 1 fi }
因此,当命令在执行过程出现error时则会被trap捕获到,然后设置错误码
1 2 3 4 5 bats_error_trap () { BATS_ERROR_STATUS="$?" BATS_ERROR_STACK_TRACE=( "${BATS_PREVIOUS_STACK_TRACE[@]} " ) trap - debug }
这样,一旦生成了错误码,则pod的状态即会出现异常状态,而正常情况下指定了restart:Never的pod执行完启动命令的状态会变成complete.
1 2 3 4 5 6 NAME READY STATUS RESTARTS AGE mongodb-rs-mongodb-replicaset-0 1/1 Running 0 4h36m mongodb-rs-mongodb-replicaset-1 1/1 Running 0 4h35m mongodb-rs-mongodb-replicaset-2 1/1 Running 0 4h35m mongodb-rs-mongodb-replicaset-test 0/1 Completed 0 4h22m
bats还有一些其它的使用技巧, 比如可以使用skip来跳过某些@test
代码块、可以用于@test
网页等.可到github 查看
不过batsN久没有更新了, 有一个改良版的bats-core ,是在bats的基本之上改良的,感兴趣的可以看看.
参考文章: