如何对operator进行集成测试
背景
operator作为一种比较新和小众的技术。只可能在少数云计算相关公司才会涉及到。自然也没有成熟的测试工具能够针对operrator进行测试。
那么在日常开发operator过程中要如何针对operator进行有效测试呢。
手动测试
在没有任何测试工具的前提下,通常是使用手动测试,比如下面几个流程
部署测试
本地IDE运行operator程序或者部署operator到集群
部署自定义资源
等待一段时间,kubectl get xxx观察 status
故障测试(比如常规的高可用测试)
持续访问该组件(while tre;do xxx;done;...)
新起一个终端, 删除pod或者关闭某台虚机
观察访问该组件的情况
功能测试(比如扩容)
更新自定义资源中的属性
等待pod更新
连接组件,指向相应的命令检测是否生效
以上是工作的日常,每次开发完一个新的功能经常需要针对上面的流程重新测试一遍,非常消耗时间。
通用测试框架
那么很多重复性的测试工作能不能采用一个通用的开发工具作为测试呢。那么对该工具提出了以下的要求。
功能
那么这一套通用测试框架需要实现哪些功能呢。
部署:能够部署多套组件(单例、集群)
故障模拟:能够自动化模拟故障
故障检测:能够判断组件是否正常
功能测试:能够针对具体命令执行测试
部署
部署通常只需要提供几套不同的yaml文件,或者helm chart的value文件,相对比较简单。
故障模拟
第一种方法是通过执行kubectl操作内部资源进行故障模拟,常用的就是执行kubectl delete pod进行删除pod。但是有个问题是重启后马上就会恢复,时间太短不好控制,可能不会触发故障触发流程。
第二种是chaosblade工具是阿里巴巴开源的混沌测试工具ChaosBlade。比较方便的是可以提供cli直接操作,比较常用的功能是让pod一直处于故障状态,使用以下命令。
blade create k8s pod-pod fail --namespace=<yournamespace> --names=<podname> --kubeconfig ~/.kube/config
其他还包含针对pod、node级别的IO、网络故障都可以用上。
故障检测:
第一种方法是依赖Operator对组件进行健康检查并维护.status.phase状态,然后通过kubectl获取该字段判断组件是否正常。当然也可以获取其他字段进行判断
kubectl get <kind> <name> -o=jsonpath='{.status.phase}'
第二种是使用组件专用的客户端进行访问,检测组件状态, 比如Mysql组件就可以使用以下脚本检查Mysql的查询功能是否正常
masterIp=$(kubectl get ep <serviceName> -ojsonpath=\"{.subsets[].addresses[].ip}\") && mysql -u root -h $masterIp -p<password>-e "select 1;"
功能测试
Operator通常只提供了修改自定义资源的方式来触发某一个功能。那么只需要
kubectl patch <kind> <name> -p '{"spec":{"replicas":3}}'
通用化流水线
测试框架还需要支持多job、多用例的流水线执行。根据要求设计了以下的流水线。
单个job主要包含 执行命令->循环验证->验证成功或超时作为主要的流程。
以下是针对公司内部mysql-operator的测试用例。
以下用例中parameter中定义变量,通常是各种常用的脚本包含kubectl、mysql、blade,以脚本的形式添加,兼容任何组件
jobs中定义了整个测试的流水线,包含mysql主库从库故障注入、主从切换验证等等常规测试流程
name: "mysql-test"
parameter:
- key: GetPhase
value: "kubectl get #{kind} #{name} -o=jsonpath='{.status.phase}'"
- key: GetRevision
value: "kubectl get #{kind} #{name} -o=jsonpath='{.metadata.resourceVersion}'"
# 删除slave
- key: DeleteAllSlave
value: "slaveLists=$(kubectl get #{kind} #{name} -o jsonpath=\"{.status.conditions[?(@.type=='Slave')].name}\");for element in ${slaveLists[@]}; do kubectl delete pod $element ;done;"
# 删除masterPod
- key: DeleteOneMaster
value: "masterLists=$(kubectl get #{kind} #{name} -o jsonpath=\"{.status.conditions[?(@.type=='Master')].name}\")&& masterLists=($masterLists) && kubectl delete pod ${masterLists[0]}"
# 创建测试库
- key: CreateTestDb
value: "masterIp=$(kubectl get ep #{name} -ojsonpath=\"{.subsets[].addresses[].ip}\") && mysql -u root -h $masterIp -pHc@Cloud01 -e \"create database if not exists operator_test\""
# 创建测试表
- key: CreateTestTable
value: "masterIp=$(kubectl get ep #{name} -ojsonpath=\"{.subsets[].addresses[].ip}\") && mysql -u root -h $masterIp -pHc@Cloud01 -e \"create table if not exists operator_test.t1 (id int);\""
# 获取master svc ip
- key: GetMasterIp
value: "kubectl get ep #{name} -ojsonpath=\"{.subsets[].addresses[].ip}\""
# 写数据
- key: WriteData
value: "masterIp=$(kubectl get ep #{name} -ojsonpath=\"{.subsets[].addresses[].ip}\") && mysql -u root -h $masterIp -pHc@Cloud01 -e \"insert into operator_test.t1 values(1);\""
# 读数据
- key: ReadData
value: "masterIp=$(kubectl get ep #{name}-readonly -ojsonpath=\"{.subsets[].addresses[].ip}\") && mysql -u root -h $masterIp -pHc@Cloud01 -e \"select 1;\""
# 使master故障
- key: MakeMasterFailed
value: "masterLists=$(kubectl get #{kind} #{name} -o jsonpath=\"{.status.conditions[?(@.type=='Master')].name}\")&& blade create k8s pod-pod fail --namespace=shenkonghui --names=${masterLists} --kubeconfig ~/.kube/config"
# 使slave故障
- key: MakeSlaveFailed
value: "slaveLists=$(kubectl get #{kind} #{name} -o jsonpath=\"{.status.conditions[?(@.type=='Slave')].name}\");for element in ${slaveLists[@]}; do blade create k8s pod-pod fail --namespace=shenkonghui --names=$element --kubeconfig ~/.kube/config ;done;"
# 清除故障测试
- key: ClearBlade
value: "kubectl delete chaosblade --all"
# 检查同步状态
#- key: CheckSync
# value: "kubectl exec -it #{name}-0 bash -- mysql -u root -S /data/mysql/db_mysql/conf/mysql.sock -pHc@Cloud01 -e \"show slave status\G\" > /tmp/test-log 2>&1 && cat /tmp/test-log| grep -E \"Slave_IO_Running:|Slave_SQL_Running:\""
- key: kind
value: mysqlcluster
- key: name
value: mysql
postJobs:
- cmd: #{ClearBlade}
jobs:
- name: "初始化状态检查"
cmd: #{ClearBlade}
verificate:
- cmd: #{GetPhase}
value: "Running"
- cmd: #{CreateTestDb}
- cmd: #{CreateTestTable}
- name: "开启协程持续写入数据"
cmd: #{WriteData}
type: parallel
- name: "主从切换—等待主库故障"
cmd: #{MakeMasterFailed}
verificate:
- cmd: #{GetPhase}
operator: "noEqual"
value: "Running"
- name: "主从切换—恢复正常"
verificate:
- cmd: #{GetPhase}
operator: "noEqual"
value: "Running"
- cmd: #{WriteData}
- cmd: #{ReadData}
- name: "初始化状态检查"
cmd: #{ClearBlade}
verificate:
- cmd: #{GetPhase}
value: "Running"
- name: "主从切换—等待从库故障"
cmd: #{MakeSlaveFailed}
verificate:
- cmd: #{GetPhase}
operator: "noEqual"
value: "Running"
- name: "主从切换—恢复正常"
timeout: 4m
verificate:
- cmd: #{GetPhase}
operator: "noEqual"
value: "Running"
- cmd: #{WriteData}
- cmd: #{ReadData}
集群中测试
通过以上测试框架和用例可以很容易在本地开发的过程中进行测试,那么在CICD中或者生产环境中部署要如何进行测试。
通常测试/交付的时候只包含镜像 + chart包。那么可以利用helm char包,将测试框架集成到helm test中。如下图的架构。
将以下测试test文件夹放在chart包的templates目录下。
test
├── configmap-single.yaml
├── configmap-cluster.yaml
└── job-test.yaml
在执行完helm install 安装完对应的组件后,即可执行helm test进行测试.