End-to-end test suite for the AxonOps Operator. Tests all 18 CRDs against a real Kubernetes cluster with a live AxonOps instance, K8ssandra Cassandra, and Strimzi Kafka.
- Install the operator via Helm chart on a fresh cluster
- Deploy AxonOps stack (
AxonOpsPlatform) and verify all components come up - Obtain an API token from the live AxonOps instance
- Deploy a 1-node Cassandra cluster (K8ssandra) and a Kafka cluster (Strimzi)
- Exercise every CRD: create, verify Ready status, verify API sync, update, delete
- Verify cleanup: finalizers run, orphaned resources removed
| Tool | Minimum Version | Purpose |
|---|---|---|
kind |
0.20+ | Create local K8s cluster (unless SKIP_CLUSTER_CREATE=true) |
kubectl |
1.28+ | Cluster interaction |
helm |
3.14+ | Install operator, cert-manager, K8ssandra, Strimzi |
curl |
any | API token creation |
jq |
any | JSON parsing |
make |
any | Build operator image |
docker |
any | Build and load images |
| Variable | Default | Description |
|---|---|---|
SKIP_CLUSTER_CREATE |
false |
Set true to use existing cluster |
KIND_CLUSTER |
axonops-e2e |
Kind cluster name |
KIND_IMAGE |
kindest/node:v1.35.0 |
Node image |
AXONOPS_HOST |
sergio.10.16.0.19.nip.io |
AxonOps dashboard URL |
AXONOPS_PROTOCOL |
https |
http or https |
AXONOPS_ORG_ID |
axonops |
Organization ID for AxonOpsConnection |
AXONOPS_CLUSTER_NAME |
e2e-test-cluster |
Cluster name used in alert CRDs |
AXONOPS_KAFKA_CLUSTER |
e2e-kafka-cluster |
Cluster name used in Kafka CRDs |
TEST_NAMESPACE |
axonops-e2e |
Namespace for test resources |
OPERATOR_IMG |
axonops-operator:e2e |
Operator image to build and load |
TIMEOUT |
300 |
Default wait timeout in seconds |
K8SSANDRA_NAMESPACE |
k8ssandra-operator |
Namespace for K8ssandra operator and cluster |
STRIMZI_NAMESPACE |
strimzi |
Namespace for Strimzi operator and Kafka cluster |
AXON_AGENT_SERVER_HOST |
axon-server-agent.$(TEST_NAMESPACE).svc.cluster.local |
Host that Cassandra/Kafka agents use to reach the AxonOps agent port (1888); override for external AxonOps deployments |
test/e2e/
├── run-e2e.sh # Main runner — calls scripts 00–99 in order
└── scripts/
├── lib.sh # Shared helpers
├── 00-setup-cluster.sh # Kind cluster + cert-manager + Gateway API
├── 01-install-operator.sh # Helm install operator
├── 02-deploy-axonops.sh # AxonOpsPlatform CR (full internal stack)
├── 03-obtain-token.sh # API token → Secret → AxonOpsConnection
├── 04-install-k8ssandra.sh # K8ssandra operator + 1-node Cassandra
├── 05-install-strimzi.sh # Strimzi operator + Kafka cluster
├── 06-test-core-crds.sh # AxonOpsPlatform + AxonOpsConnection
├── 07-test-alert-crds.sh # MetricAlert, LogAlert, AlertRoute, AlertEndpoint
├── 08-test-healthcheck-crds.sh # HealthcheckHTTP, HealthcheckTCP, HealthcheckShell
├── 09-test-ops-crds.sh # DashboardTemplate, AdaptiveRepair, ScheduledRepair, CommitlogArchive
├── 10-test-backup-crds.sh # AxonOpsBackup
├── 11-test-kafka-crds.sh # KafkaTopic, KafkaACL, KafkaConnector
├── 12-test-cleanup.sh # Delete all CRs, verify finalizers and resource removal
└── 99-teardown.sh # Uninstall everything, destroy Kind cluster
# Wait for a condition on a resource (e.g., Ready=True)
wait_for_condition <resource> <name> <condition> <timeout>
# Assert a kubectl jsonpath equals an expected value
assert_jsonpath <resource> <name> <jsonpath> <expected>
# Assert a Kubernetes resource exists / does not exist
assert_exists <resource> <name>
assert_not_exists <resource> <name>
# Apply YAML from heredoc, wait for Ready, return
apply_and_wait <timeout> # reads YAML from stdin
# Delete resource and wait for it to be gone (finalizer completion)
delete_and_wait <resource> <name> <timeout>
# Log pass/fail with color output
pass <message>
fail <message>
# Counter for pass/fail summary
TESTS_PASSED=0
TESTS_FAILED=0
print_summary- If
SKIP_CLUSTER_CREATE != true:kind create cluster --name $KIND_CLUSTER --image=$KIND_IMAGE
- Install cert-manager:
helm upgrade --install cert-manager oci://quay.io/jetstack/charts/cert-manager \ --version v1.29.0 --namespace cert-manager --create-namespace \ --set crds.enabled=true --wait
- Install Gateway API CRDs (Envoy Gateway):
helm install eg oci://docker.io/envoyproxy/gateway-helm \ --version v1.7.1 -n envoy-gateway-system --create-namespace --wait
- Create test namespace:
kubectl create namespace $TEST_NAMESPACE
- Build operator image:
make docker-build IMG=$OPERATOR_IMG - Load into Kind:
kind load docker-image $OPERATOR_IMG --name $KIND_CLUSTER2.1 if not using kind, push the image tottl.sh:
OPERATOR_IMG=ttl.sh/axonops-operator:${RANDOM_UUID}
docker push $OPERATOR_IMG- Install via Helm:
helm upgrade --install axonops-operator ./charts/axonops-operator/ \ --namespace axonops-operator-system --create-namespace \ --set manager.image.repository=${OPERATOR_IMG%:*} \ --set manager.image.tag=${OPERATOR_IMG#*:} \ --wait --timeout 120s
- Verify controller pod is Running:
kubectl wait --for=condition=Available deployment/axonops-operator-controller-manager \ -n axonops-operator-system --timeout=120s
- Apply
AxonOpsPlatformCR with all internal components enabled:apiVersion: core.axonops.com/v1alpha1 kind: AxonOpsPlatform metadata: name: axonops namespace: $TEST_NAMESPACE spec: server: orgName: "$AXONOPS_ORG_ID" timeSeries: enabled: true search: enabled: true dashboard: enabled: true
- Wait for status conditions:
CertManagerReady=True(timeout: 120s)ServerReady=True(timeout: 300s)DashboardReady=True(timeout: 120s)
- Verify created resources:
- 4 StatefulSets (axon-server, axondb-timeseries, axondb-search) + 1 Deployment (axon-dash)
- Services for each component
- Auth Secrets for timeSeries and search
- TLS Certificate Secrets
- ConfigMap for dashboard
- Request API token:
RESPONSE=$(curl -sk -X POST \ "${AXONOPS_PROTOCOL}://${AXONOPS_HOST}/api/v1/axonops/createApiToken" \ -H "Content-Type: application/json" \ -d '{"allowed_roles":["_global_/superuser"],"token_expiry_time":0}') API_KEY=$(echo "$RESPONSE" | jq -r '.apiKey')
- Create Secret:
apiVersion: v1 kind: Secret metadata: name: axonops-api-key namespace: $TEST_NAMESPACE stringData: api-key: "$API_KEY"
- Create AxonOpsConnection:
apiVersion: core.axonops.com/v1alpha1 kind: AxonOpsConnection metadata: name: e2e-connection namespace: $TEST_NAMESPACE spec: orgId: "$AXONOPS_ORG_ID" host: "$AXONOPS_HOST" protocol: "$AXONOPS_PROTOCOL" tlsSkipVerify: true apiKeyRef: name: axonops-api-key key: api-key
- Verify Connection resource exists and Secret is populated.
-
Install K8ssandra operator:
helm repo add k8ssandra https://helm.k8ssandra.io/stable helm install k8ssandra-operator k8ssandra/k8ssandra-operator \ -n k8ssandra-operator --create-namespace --wait --version 0.30.0
-
Apply 1-node K8ssandraCluster CR:
envsubst < test/e2e/manifests/k8ssandra-cluster.yaml | kubectl apply -f -
-
Wait for Cassandra pod to be Ready:
kubectl wait pod -l app.kubernetes.io/created-by=cass-operator --for=condition=Ready \ -n k8ssandra-operator --timeout=600s
-
Install Strimzi operator:
helm repo add strimzi https://strimzi.io/charts/ helm install strimzi-kafka-operator strimzi/strimzi-kafka-operator \ -n strimzi --create-namespace --wait --version 0.50.1
-
Apply Kafka cluster CR:
envsubst < test/e2e/manifests/kafka-cluster.yaml | kubectl apply -f -
-
Wait for Kafka pods to be Ready:
kubectl wait kafka/${AXONOPS_KAFKA_CLUSTER} --for=condition=Ready \ -n ${STRIMZI_NAMESPACE} --timeout=600s
Every test follows the same pattern:
- Create — apply the CR
- Wait —
wait_for_condition <resource> <name> Ready True $TIMEOUT - Assert status — check
syncedAlertID(or equivalent) is non-empty - Update — modify a spec field, re-apply
- Wait — confirm
observedGenerationincrements and Ready remains True - Delete —
delete_and_waitconfirms resource is fully removed
Already deployed in Phase 1. Additional assertions:
status.timeSeriesSecretNameis non-emptystatus.searchSecretNameis non-emptystatus.timeSeriesCertSecretNameis non-emptystatus.searchCertSecretNameis non-empty- All 4 StatefulSet/Deployment replicas are Ready
- Services resolve within the cluster
Already created in Phase 2. Additional assertions:
- Resource exists with correct
orgId - Referenced Secret exists and has
api-keydata key - Update: change
tlsSkipVerify→ verifyobservedGenerationincrements
apiVersion: alerts.axonops.com/v1alpha1
kind: AxonOpsMetricAlert
metadata:
name: io-wait
namespace: $TEST_NAMESPACE
spec:
connectionRef: e2e-connection
clusterName: $AXONOPS_CLUSTER_NAME
clusterType: cassandra
name: io-wait
operator: ">="
warningValue: 20
criticalValue: 50
duration: 15m
dashboard: System
chart: Avg IO wait CPU per Host- Assert:
status.conditions[Ready]=True,status.syncedAlertIDnon-empty - Update: change
criticalValueto 200, verify re-sync - Delete: verify finalizer removes alert from AxonOps API
apiVersion: alerts.axonops.com/v1alpha1
kind: AxonOpsLogAlert
metadata:
name: e2e-log-alert
namespace: $TEST_NAMESPACE
spec:
connectionRef: e2e-connection
clusterName: $AXONOPS_CLUSTER_NAME
clusterType: cassandra
name: e2e-tombstone-alert
operator: ">="
warningValue: 1
criticalValue: 5
duration: 15m
level: "error"
content: "TombstoneOverwhelmingException"
source: "/var/log/cassandra/system.log"- Assert:
Ready=True,syncedAlertIDnon-empty - Update: change
criticalValue, verify re-sync - Delete: finalizer cleanup
apiVersion: alerts.axonops.com/v1alpha1
kind: AxonOpsAlertRoute
metadata:
name: e2e-alert-route
namespace: $TEST_NAMESPACE
spec:
connectionRef: e2e-connection
clusterName: $AXONOPS_CLUSTER_NAME
clusterType: cassandra
integrationName: e2e-slack-channel
integrationType: slack
type: metrics
severity: error
enableOverride: true- Assert:
Ready=True,syncedRouteIDnon-empty - Update: change
severitytowarning, verify re-sync - Delete: finalizer cleanup
apiVersion: alerts.axonops.com/v1alpha1
kind: AxonOpsAlertEndpoint
metadata:
name: e2e-alert-endpoint
namespace: $TEST_NAMESPACE
spec:
connectionRef: e2e-connection
clusterName: $AXONOPS_CLUSTER_NAME
clusterType: cassandra
name: e2e-webhook-endpoint
type: webhook
webhook:
url: "https://httpbin.org/post"- Assert:
Ready=True,syncedEndpointIDnon-empty - Delete: finalizer cleanup
apiVersion: alerts.axonops.com/v1alpha1
kind: AxonOpsHealthcheckHTTP
metadata:
name: e2e-healthcheck-http
namespace: $TEST_NAMESPACE
spec:
connectionRef: e2e-connection
clusterName: $AXONOPS_CLUSTER_NAME
clusterType: cassandra
name: e2e-http-check
url: "https://httpbin.org/status/200"
method: GET
expectedStatus: 200
interval: 60s
timeout: 10s- Assert:
Ready=True,syncedHealthcheckIDnon-empty - Delete: finalizer cleanup
apiVersion: alerts.axonops.com/v1alpha1
kind: AxonOpsHealthcheckTCP
metadata:
name: e2e-healthcheck-tcp
namespace: $TEST_NAMESPACE
spec:
connectionRef: e2e-connection
clusterName: $AXONOPS_CLUSTER_NAME
clusterType: cassandra
name: e2e-tcp-check
tcp: "localhost:9042"
interval: 60s
timeout: 10s- Assert:
Ready=True,syncedHealthcheckIDnon-empty - Delete: finalizer cleanup
apiVersion: alerts.axonops.com/v1alpha1
kind: AxonOpsHealthcheckShell
metadata:
name: e2e-healthcheck-shell
namespace: $TEST_NAMESPACE
spec:
connectionRef: e2e-connection
clusterName: $AXONOPS_CLUSTER_NAME
clusterType: cassandra
name: e2e-shell-check
script: "echo ok"
shell: "/bin/bash"
interval: 60s
timeout: 10s- Assert:
Ready=True,syncedHealthcheckIDnon-empty - Delete: finalizer cleanup
apiVersion: alerts.axonops.com/v1alpha1
kind: AxonOpsDashboardTemplate
metadata:
name: e2e-dashboard-template
namespace: $TEST_NAMESPACE
spec:
connectionRef: e2e-connection
clusterName: $AXONOPS_CLUSTER_NAME
clusterType: cassandra
dashboardName: "E2E Test Dashboard"
source:
inline:
dashboard:
filters: []
panels:
- title: "Test Panel"
type: line-chart
layout: { x: 0, y: 0, w: 12, h: 8 }
details:
queries:
- query: "test_metric"
legend: "Test"- Assert:
Ready=True,syncedDashboardIDnon-empty - Delete: finalizer cleanup
apiVersion: alerts.axonops.com/v1alpha1
kind: AxonOpsAdaptiveRepair
metadata:
name: e2e-adaptive-repair
namespace: $TEST_NAMESPACE
spec:
connectionRef: e2e-connection
clusterName: $AXONOPS_CLUSTER_NAME
clusterType: cassandra
active: true
tableParallelism: 2
gcGraceThreshold: 172800- Assert:
Ready=True,syncedRepairIDnon-empty - Update: change
tableParallelismto 4, verify re-sync - Delete: finalizer cleanup
apiVersion: alerts.axonops.com/v1alpha1
kind: AxonOpsScheduledRepair
metadata:
name: e2e-scheduled-repair
namespace: $TEST_NAMESPACE
spec:
connectionRef: e2e-connection
clusterName: $AXONOPS_CLUSTER_NAME
clusterType: cassandra
tag: e2e-repair
scheduleExpression: "0 0 1 * *"
parallelism: DC-Aware
segmented: true
segmentsPerNode: 4- Assert:
Ready=True,syncedRepairIDnon-empty - Delete: finalizer cleanup
apiVersion: alerts.axonops.com/v1alpha1
kind: AxonOpsCommitlogArchive
metadata:
name: e2e-commitlog-archive
namespace: $TEST_NAMESPACE
spec:
connectionRef: e2e-connection
clusterName: $AXONOPS_CLUSTER_NAME
clusterType: cassandra
remoteType: local
remotePath: /tmp/commitlogs
remoteRetention: "7d"
timeout: "2h"- Assert:
Ready=True,syncedArchiveIDnon-empty - Delete: finalizer cleanup
apiVersion: backups.axonops.com/v1alpha1
kind: AxonOpsBackup
metadata:
name: e2e-backup
namespace: $TEST_NAMESPACE
spec:
connectionRef: e2e-connection
clusterName: $AXONOPS_CLUSTER_NAME
clusterType: cassandra
tag: e2e-daily-backup
schedule: true
scheduleExpression: "0 1 * * *"
localRetention: "7d"
timeout: "4h"- Assert:
Ready=True,syncedBackupIDnon-empty - Update: change
localRetentionto14d, verify re-sync - Delete: finalizer cleanup
apiVersion: kafka.axonops.com/v1alpha1
kind: AxonOpsKafkaTopic
metadata:
name: e2e-kafka-topic
namespace: $TEST_NAMESPACE
spec:
connectionRef: e2e-connection
clusterName: $AXONOPS_KAFKA_CLUSTER
name: e2e-test-topic
partitions: 3
replicationFactor: 1
config:
cleanup.policy: "delete"
retention.ms: "86400000"- Assert:
Ready=True,syncedTopicIDnon-empty - Update: change
partitionsto 6, verify re-sync - Delete: finalizer cleanup
apiVersion: kafka.axonops.com/v1alpha1
kind: AxonOpsKafkaACL
metadata:
name: e2e-kafka-acl
namespace: $TEST_NAMESPACE
spec:
connectionRef: e2e-connection
clusterName: $AXONOPS_KAFKA_CLUSTER
resourceType: TOPIC
resourceName: e2e-test-topic
resourcePatternType: LITERAL
principal: "User:e2e-producer"
host: "*"
operation: WRITE
permissionType: ALLOW- Assert:
Ready=True,syncedACLIDnon-empty - Delete: finalizer cleanup
apiVersion: kafka.axonops.com/v1alpha1
kind: AxonOpsKafkaConnector
metadata:
name: e2e-kafka-connector
namespace: $TEST_NAMESPACE
spec:
connectionRef: e2e-connection
clusterName: $AXONOPS_KAFKA_CLUSTER
connectClusterName: e2e-connect
name: e2e-file-sink
config:
connector.class: "org.apache.kafka.connect.file.FileStreamSinkConnector"
topics: "e2e-test-topic"
file: "/tmp/e2e-output.txt"
tasks.max: "1"- Assert:
Ready=True,syncedConnectorIDnon-empty - Delete: finalizer cleanup
Tests that deleting CRs triggers proper finalizer execution and resource cleanup.
-
Delete all alert/ops CRs created in Phase 4
- For each:
delete_and_wait— confirm the CR is fully removed - Verify synced resources are removed from AxonOps API (optional: curl the API to confirm)
- For each:
-
Delete AxonOpsConnection
- Verify Secret still exists (not owned by connection)
- Verify connection CR is gone
-
Disable AxonOpsPlatform components one by one
- Patch
dashboard.enabled: false→ verify Deployment, Service, ConfigMap deleted - Patch
search.enabled: false→ verify StatefulSet, Services, Secrets, TLS cert deleted - Patch
timeSeries.enabled: false→ verify StatefulSet, Services, Secrets, TLS cert deleted
- Patch
-
Delete AxonOpsPlatform
- Verify all remaining resources cleaned up (server StatefulSet, Services, etc.)
- Verify namespace has no orphaned resources with
app.kubernetes.io/managed-by: axonops-operator
- Uninstall operator:
helm uninstall axonops-operator -n axonops-operator-system - Uninstall K8ssandra:
helm uninstall k8ssandra-operator -n k8ssandra-operator - Uninstall Strimzi:
helm uninstall strimzi-kafka-operator -n strimzi - Uninstall cert-manager:
helm uninstall cert-manager -n cert-manager - Delete namespaces
- If
SKIP_CLUSTER_CREATE != true:kind delete cluster --name $KIND_CLUSTER
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/scripts" && pwd)"
source "$SCRIPT_DIR/lib.sh"
SCRIPTS=(
00-setup-cluster
01-install-operator
02-deploy-axonops
03-obtain-token
04-install-k8ssandra
05-install-strimzi
06-test-core-crds
07-test-alert-crds
08-test-healthcheck-crds
09-test-ops-crds
10-test-backup-crds
11-test-kafka-crds
12-test-cleanup
)
# Allow running a single phase: ./run-e2e.sh 07
if [[ ${1:-} ]]; then
SCRIPTS=("$1")
fi
for script in "${SCRIPTS[@]}"; do
echo "=== Running $script ==="
bash "$SCRIPT_DIR/${script}.sh"
done
print_summary
# Always teardown unless SKIP_TEARDOWN=true
if [[ "${SKIP_TEARDOWN:-false}" != "true" ]]; then
bash "$SCRIPT_DIR/99-teardown.sh"
fi| # | CRD | Script | Create | Wait Condition | Status Field | Update | Delete |
|---|---|---|---|---|---|---|---|
| 1 | AxonOpsPlatform | 06 | Phase 1 | ServerReady=True | timeSeriesSecretName | n/a (tested in 12) | 12 |
| 2 | AxonOpsConnection | 06 | Phase 2 | exists | orgId | tlsSkipVerify | 12 |
| 3 | AxonOpsMetricAlert | 07 | YAML | Ready=True | syncedAlertID | criticalValue | 07+12 |
| 4 | AxonOpsLogAlert | 07 | YAML | Ready=True | syncedAlertID | criticalValue | 07+12 |
| 5 | AxonOpsAlertRoute | 07 | YAML | Ready=True | syncedRouteID | severity | 07+12 |
| 6 | AxonOpsAlertEndpoint | 07 | YAML | Ready=True | syncedEndpointID | — | 07+12 |
| 7 | AxonOpsHealthcheckHTTP | 08 | YAML | Ready=True | syncedHealthcheckID | — | 08+12 |
| 8 | AxonOpsHealthcheckTCP | 08 | YAML | Ready=True | syncedHealthcheckID | — | 08+12 |
| 9 | AxonOpsHealthcheckShell | 08 | YAML | Ready=True | syncedHealthcheckID | — | 08+12 |
| 10 | AxonOpsDashboardTemplate | 09 | YAML | Ready=True | syncedDashboardID | — | 09+12 |
| 11 | AxonOpsAdaptiveRepair | 09 | YAML | Ready=True | syncedRepairID | tableParallelism | 09+12 |
| 12 | AxonOpsScheduledRepair | 09 | YAML | Ready=True | syncedRepairID | — | 09+12 |
| 13 | AxonOpsCommitlogArchive | 09 | YAML | Ready=True | syncedArchiveID | — | 09+12 |
| 14 | AxonOpsBackup | 10 | YAML | Ready=True | syncedBackupID | localRetention | 10+12 |
| 15 | AxonOpsKafkaTopic | 11 | YAML | Ready=True | syncedTopicID | partitions | 11+12 |
| 16 | AxonOpsKafkaACL | 11 | YAML | Ready=True | syncedACLID | — | 11+12 |
| 17 | AxonOpsKafkaConnector | 11 | YAML | Ready=True | syncedConnectorID | — | 11+12 |
| 18 | AxonOpsPlatform (external) | 06 | optional | ServerReady=True | — | — | — |
User-provided manifests go under test/e2e/manifests/:
test/e2e/manifests/
├── k8ssandra-cluster.yaml # 1-node K8ssandraCluster (user provides)
├── kafka-cluster.yaml # Strimzi Kafka cluster (user provides)
└── (generated at runtime by scripts)
.PHONY: test-e2e-full
test-e2e-full:
bash test/e2e/run-e2e.shname: E2E Full Suite
on:
workflow_dispatch: # manual trigger only — requires live AxonOps
# schedule:
# - cron: '0 2 * * 1' # optional: weekly Monday 2am
jobs:
e2e:
runs-on: ubuntu-latest
env:
AXONOPS_HOST: ${{ secrets.AXONOPS_HOST }}
AXONOPS_ORG_ID: ${{ secrets.AXONOPS_ORG_ID }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install Kind
run: go install sigs.k8s.io/kind@latest
- name: Run E2E Suite
run: make test-e2e-full
- name: Collect logs on failure
if: failure()
run: |
kubectl logs -n axonops-operator-system deployment/axonops-operator-controller-manager -c manager --tail=200 || true
kubectl get events -n $TEST_NAMESPACE --sort-by='.lastTimestamp' || true| Phase | Scripts | Estimated Duration |
|---|---|---|
| Infrastructure | 00–02 | ~5–8 min (cert-manager + operator + AxonOps stack) |
| Auth | 03 | ~10 sec |
| Data platforms | 04–05 | ~5–10 min (Cassandra + Kafka startup) |
| CRD tests | 06–11 | ~5–10 min (18 CRDs, mostly API sync waits) |
| Cleanup tests | 12 | ~2–3 min |
| Teardown | 99 | ~1 min |
| Total | ~20–30 min |