Skip to content

Commit

Permalink
test: better automated e2e tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Massimo Gengarelli committed Jul 2, 2024
1 parent e6e6ee1 commit a6ba5d0
Show file tree
Hide file tree
Showing 3 changed files with 324 additions and 14 deletions.
15 changes: 7 additions & 8 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,11 @@ jobs:
with:
skipClusterCreation: true
- run: terraform init
- run: make cluster-test
- run: sleep 30
- run: kubectl get ns | grep target
- run: kubectl get ns | grep chaosmonkey
- run: kubectl -n chaosmonkey get pods | grep chaos-monkey
- run: kubectl -n target get deployments | grep nginx
- run: kubectl -n target get cmc | grep chaosmonkey-nginx
- run: kubectl -n target get events | grep ChaosMonkey
name: Initialize Terraform
- run: |
make cluster-test
sleep 10
./tests/kubetest.sh
name: End to end tests
- run: make clean
name: Cleanup
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,22 @@ pull request. If you want to develop and test locally, you need to install:
- [Kind](https://github.com/kubernetes-sigs/kind) at least version 0.5.0
- [Kubectl](https://kubernetes.io/docs/tasks/tools/) at least version 1.30.1

The reason why you need so many tools, is because there is an embedded
[terraform file](./main.tf) which spawns a Kubernetes cluster in your local machine
using Kind, injects the CRD, builds the image and gives you a working environment
to test your development. There are some unit tests included, but since all the
interactions with Kubernetes are mocked, we cannot be sure that everything will
work once deployed.
### Unit Tests
The project includes a wide variety of unit tests, which are using the `fake` client
of kubernetes included in the `client-go` library. The problem is that when testing
with mocks, most of the times you end up testing the mocks and not the code. That's
the reason why there are also some [integration tests](#integration-tests) included.

### Integration Tests
These tests should cover the basic functionalities of the Chaos Monkey in a local
Kubernetes cluster. The script file is [here](./tests/kubetest.sh) and before launching
it you should create the Kubernetes cluster locally, using the included [Terraform](./main.tf) configuration.

It should be as easy as launching:

$ make cluster-test
$ ./tests/kubetest.sh

You can also activate a more verbose logging for the tests with

TEST_DEBUG=true ./tests/kubetest.sh
298 changes: 298 additions & 0 deletions tests/kubetest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
#!/usr/bin/env bash

KUBECTL=$(which kubectl)
CLUSTER_NAME="${TERRAFORM_CLUSTER_NAME:-chaosmonkey-cluster}"

set -eo pipefail

log() {
local level="${1}"
local coloredLevel=""
shift
case "${level}" in
info)
coloredLevel="\033[0;32m${level}\033[0m"
;;
warn)
coloredLevel="\033[0;33m${level}\033[0m"
;;
error)
coloredLevel="\033[0;31m${level}\033[0m"
;;
debug)
coloredLevel="\033[0;34m${level}\033[0m"
;;
*)
coloredLevel="\033[0;32munknown\033[0m"
;;

esac

echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] [${coloredLevel}] - $*"
}

info() {
log info "$*"
}

warn() {
log warn "$*"
}

debug() {
if [[ "${TEST_DEBUG:-false}" == "true" ]]; then
log debug "$*"
fi
}

err() {
log error "$*"
exit 1
}

# Helper function to check that the replicas of a given deployment change over time
checkReplicas() {
local deploymentName="$1"
local currentScale
local newScale
local completedLoops

currentScale=$(${KUBECTL} get deployment "${deploymentName}" --namespace=target -o jsonpath='{.spec.replicas}')
newScale=$(${KUBECTL} get deployment "${deploymentName}" --namespace=target -o jsonpath='{.spec.replicas}')
debug "Checking replicas for ${deploymentName}"

completedLoops=0
while [ "${currentScale}" == "${newScale}" ]; do
info "Current replicas: ${currentScale}, waiting for the replicas to change ($((10 - completedLoops)) retries left)"

newScale=$(${KUBECTL} get deployment "${deploymentName}" --namespace=target -o jsonpath='{.spec.replicas}')
debug "Current replicas: ${currentScale}, new replicas: ${newScale}"

completedLoops=$((completedLoops + 1))
if [ ${completedLoops} -gt 10 ]; then
err "Replicas did not change after ${completedLoops} retries, please check the chaosmonkey pod logs"
else
sleep 10
fi
done
}

# Helper function to verify that the scale of a given deployment *does not* change over time
checkReplicasDoesNotChange() {
local deploymentName="$1"
local currentScale
local newScale
local completedLoops

debug "Checking that .spec.replicas for ${deploymentName} does not change over time"

currentScale=$(${KUBECTL} get deployment "${deploymentName}" --namespace=target -o jsonpath='{.spec.replicas}')
newScale=$(${KUBECTL} get deployment "${deploymentName}" --namespace=target -o jsonpath='{.spec.replicas}')

completedLoops=0
while [ ${completedLoops} -lt 5 ]; do
debug "Loop #${completedLoops}"

if [ "${currentScale}" != "${newScale}" ]; then
err "Number of replicas changed (${currentScale} -> ${newScale})"
else
info "Still ok, number of replicas: ${currentScale} ($((5 - completedLoops)) loops left)"
sleep 5
newScale=$(${KUBECTL} get deployment "${deploymentName}" --namespace=target -o jsonpath='{.spec.replicas}')
completedLoops=$((completedLoops + 1))
fi
done
}

# Helper function to check that the pods of a given deployment are recreated over time
checkPods() {
local selector="$1"
local currentPods
local newPods
local completedLoops

currentPods=$(${KUBECTL} get -n target pods --selector "${selector}" -o jsonpath='{.items[*].metadata.name}')
newPods=$(${KUBECTL} get -n target pods --selector "${selector}" -o jsonpath='{.items[*].metadata.name}')

debug "Checking pods for ${selector}"

completedLoops=0
while [ "${currentPods}" == "${newPods}" ]; do
info "Current pods: ${currentPods}, waiting for the pods to change ($((10 - completedLoops)) retries left)"

newPods=$(${KUBECTL} get -n target pods --selector "${selector}" -o jsonpath='{.items[*].metadata.name}')
debug "Current pods: ${currentPods}, new pods: ${newPods}"

completedLoops=$((completedLoops + 1))
if [ ${completedLoops} -gt 10 ]; then
err "Pods did not change after ${completedLoops} retries, please check the chaosmonkey pod logs"
else
sleep 10
fi
done
}

# Helper function to check that the number of pods stay equal over time
checkNumberPods() {
local selector="$1"
local targetValue=$2
local countPods
local completedLoops

debug "Checking number of pods for ${selector} (should remain ${targetValue})"

countPods=$(${KUBECTL} get -n target pods --selector "${selector}" --no-headers | wc -l)

completedLoops=0
info "Checking number of pods"
while [[ ${countPods} != "${targetValue}" ]]; do
debug "Checking number of pods (${countPods}), waiting for ${targetValue} ($((10 - completedLoops)) retries left)"
countPods=$(${KUBECTL} get -n target pods --selector "${selector}" --no-headers | wc -l)
sleep 1

completedLoops=$((completedLoops + 1))
if [ ${completedLoops} -gt 10 ]; then
err "There should always be 2 pods active"
fi
done
}

debug "Checking kubectl @ ${KUBECTL}"
if [[ -z "${KUBECTL}" ]]; then
err "Please install kubectl: https://kubernetes.io/docs/tasks/tools/install-kubectl/"
fi
info "Kubectl found at ${KUBECTL}"

# Check if the cluster has been started
debug "Check that ${CLUSTER_NAME} exists"
if ! ${KUBECTL} config get-contexts | grep "kind-${CLUSTER_NAME}" &>/dev/null; then
err "Please start the cluster using 'make cluster-test' before running this script"
else
debug "Force switching context to ${CLUSTER_NAME}"
${KUBECTL} config use-context "kind-${CLUSTER_NAME}" >/dev/null
fi
info "Cluster ${CLUSTER_NAME} found"

# Start the test
info "Starting the test"

info "Checking namespaces"
for ns in target chaosmonkey; do
debug "Checking if namespace ${ns} target exists"
if ! ${KUBECTL} get ns | grep ${ns} &>/dev/null; then
err "Namespace ${ns} does not exist"
fi
done

info "Checking pods"
for ns in target chaosmonkey; do
debug "Checking if pods in namespace ${ns} are ready"
if ! ${KUBECTL} get pods --namespace=${ns} | grep Running &>/dev/null; then
err "Pods in namespace ${ns} target are not ready"
fi
done

info "Checking deployments"
deploymentCount=$(${KUBECTL} get deployments --namespace=chaosmonkey --no-headers | wc -l)
debug "chaosmonkey namespace contains ${deploymentCount} deployment(s)"
if [[ ${deploymentCount} != 1 ]]; then
err "chaosmonkey namespace should contain 1 deployment"
fi

deploymentCount=$(${KUBECTL} get deployments --namespace=target --no-headers | wc -l)
debug "target namespace contains ${deploymentCount} deployment(s)"
if [[ ${deploymentCount} != 2 ]]; then
err "target namespace should contain 2 deployments"
fi

info "Checking ChaosMonkeyConfigurations"
cmcCount=$(${KUBECTL} get cmc --namespace=target --no-headers | wc -l)
debug "target namespace contains ${cmcCount} cmc(s)"
if [[ ${cmcCount} != 2 ]]; then
err "target namespace should contain 2 cmc"
fi

disruptScale="nginx-disrupt-scale"
disruptPods="nginx-disrupt-pods"

info "Resetting CMCs to initial values"

debug "Force enable ${disruptScale}"
${KUBECTL} -n target patch cmc chaosmonkey-${disruptScale} --type json --patch-file=/dev/stdin <<-JSONPATCH >/dev/null
[
{"op": "replace", "path": "/spec/enabled", "value": true},
{"op": "replace", "path": "/spec/podMode", "value": false},
{"op": "replace", "path": "/spec/minReplicas", "value": 2},
{"op": "replace", "path": "/spec/maxReplicas", "value": 5}
]
JSONPATCH

debug "Force enable ${disruptPods}"
${KUBECTL} -n target patch cmc chaosmonkey-${disruptPods} --type json --patch-file=/dev/stdin <<-JSONPATCH >/dev/null
[
{"op": "replace", "path": "/spec/enabled", "value": true},
{"op": "replace", "path": "/spec/podMode", "value": true},
{"op": "replace", "path": "/spec/minReplicas", "value": 6},
{"op": "replace", "path": "/spec/maxReplicas", "value": 8}
]
JSONPATCH

info "Resetting ${disruptPods} to 2 replicas"
${KUBECTL} -n target scale deployment ${disruptPods} --replicas=2 >/dev/null

info "Checking events"
if ! ${KUBECTL} -n target get events | grep ChaosMonkey &>/dev/null; then
warn "no events found in target namespace, please check the chaosmonkey pod logs (not considered as an error)"
fi

info "Checking CMC with podMode=false (${disruptScale})"
checkReplicas ${disruptScale}

info "Checking CMC with podMode=true (${disruptPods})"
checkPods "app=${disruptPods}"

info "Checking number of pods"
checkNumberPods "app=${disruptPods}" 2

info "Stopping ${disruptScale} CMC"
if ! ${KUBECTL} patch -n target cmc chaosmonkey-${disruptScale} --type json --patch-file=/dev/stdin <<-JSONPATCH >/dev/null; then
[
{ "op": "replace", "path": "/spec/enabled", "value": false }
]
JSONPATCH
err "Could not patch CMC for ${disruptScale}"
fi

info "Checking that CMC ${disruptScale} has been stopped correctly (number of scales should not change over time)"
checkReplicasDoesNotChange ${disruptScale}

info "Switching ${disruptPods} from podMode=true to podMode=false"
if ! ${KUBECTL} patch -n target cmc chaosmonkey-${disruptPods} --type json --patch '[{"op":"replace", "path":"/spec/podMode", "value":false}]' >/dev/null; then
err "Could not patch CMC ${disruptPods}"
fi

info "Checking that CMC ${disruptPods} is now correctly modifying the replicas of the deployment"
checkReplicas ${disruptPods}

info "Switching ${disruptScale} from podMode=false to podMode=true and re-enabling it"
if ! ${KUBECTL} patch -n target cmc chaosmonkey-${disruptScale} --type json --patch-file=/dev/stdin <<-JSONPATCH >/dev/null; then
[
{ "op": "replace", "path": "/spec/enabled", "value": true },
{ "op": "replace", "path": "/spec/podMode", "value": true }
]
JSONPATCH
err "Could not patch CMC ${disruptScale}"
fi

info "Making sure there are at least two replicas of ${disruptScale}"
if ! ${KUBECTL} scale -n target deployment ${disruptScale} --replicas=2 >/dev/null; then
err "Could not scale ${disruptScale}"
fi

info "Checking that pods change over time"
checkPods "app=${disruptScale}"

info "Checking that we still have 2 pods"
checkNumberPods "app=${disruptScale}" 2

info "All tests passed!"

0 comments on commit a6ba5d0

Please sign in to comment.