@@ -3737,8 +3737,237 @@ phase_overlay_tests() {
37373737 run_test test_overlay_reset_generation " ReaperOverlay reset generation" --hard-fail
37383738 run_test test_overlay_delete_cleanup " ReaperOverlay delete and finalizer cleanup" --hard-fail
37393739
3740- # Cleanup overlay resources, then controller ( deferred from Phase 4b )
3740+ # Cleanup overlay resources ( controller cleanup deferred to after Phase 4d )
37413741 cleanup_overlay
3742+ }
3743+
3744+ # ---------------------------------------------------------------------------
3745+ # Phase 4d: ReaperDaemonJob CRD tests
3746+ # ---------------------------------------------------------------------------
3747+
3748+ test_daemon_job_crd_install () {
3749+ # CRD is installed by Helm during setup. Apply idempotently in case of reruns.
3750+ kubectl apply -f deploy/kubernetes/crds/reaperdaemonjobs.reaper.io.yaml >> " $LOG_FILE " 2>&1
3751+
3752+ # Verify CRD is established
3753+ local established=" "
3754+ for i in $( seq 1 15) ; do
3755+ established=$( kubectl get crd reaperdaemonjobs.reaper.io -o jsonpath=' {.status.conditions[?(@.type=="Established")].status}' 2> /dev/null || true)
3756+ if [[ " $established " == " True" ]]; then
3757+ break
3758+ fi
3759+ sleep 1
3760+ done
3761+ [[ " $established " == " True" ]] || {
3762+ log_error " CRD reaperdaemonjobs.reaper.io not established after 15s"
3763+ return 1
3764+ }
3765+
3766+ # Verify we can list ReaperDaemonJobs
3767+ kubectl get reaperdaemonjobs --all-namespaces --no-headers >> " $LOG_FILE " 2>&1 || {
3768+ log_error " Cannot list ReaperDaemonJobs"
3769+ return 1
3770+ }
3771+ }
3772+
3773+ test_daemon_job_simple () {
3774+ # Create a simple ReaperDaemonJob that runs on all nodes
3775+ kubectl apply -f - << 'YAML '
3776+ apiVersion: reaper.io/v1alpha1
3777+ kind: ReaperDaemonJob
3778+ metadata:
3779+ name: test-simple-djob
3780+ spec:
3781+ command: ["/bin/sh", "-c"]
3782+ args:
3783+ - echo "hello-from-daemonjob on $(hostname)"
3784+ YAML
3785+
3786+ # Wait for ReaperPods to be created (one per ready node)
3787+ local rp_count=0
3788+ for i in $( seq 1 30) ; do
3789+ rp_count=$( kubectl get reaperpods -l reaper.io/daemon-job=test-simple-djob --no-headers 2> /dev/null | wc -l | tr -d ' ' || echo " 0" )
3790+ if [[ " $rp_count " -ge 1 ]]; then
3791+ break
3792+ fi
3793+ sleep 1
3794+ done
3795+ [[ " $rp_count " -ge 1 ]] || {
3796+ log_error " No ReaperPods created for ReaperDaemonJob test-simple-djob after 30s"
3797+ return 1
3798+ }
3799+ log_verbose " ReaperDaemonJob created $rp_count ReaperPod(s)"
3800+ }
3801+
3802+ test_daemon_job_status_tracking () {
3803+ # Wait for the DaemonJob to complete (all nodes Succeeded)
3804+ local phase=" "
3805+ for i in $( seq 1 60) ; do
3806+ phase=$( kubectl get reaperdaemonjob test-simple-djob -o jsonpath=' {.status.phase}' 2> /dev/null || true)
3807+ if [[ " $phase " == " Completed" ]]; then
3808+ break
3809+ fi
3810+ sleep 2
3811+ done
3812+ [[ " $phase " == " Completed" ]] || {
3813+ log_error " Expected ReaperDaemonJob phase=Completed, got '$phase '"
3814+ return 1
3815+ }
3816+
3817+ # Verify ready/total counts match
3818+ local ready total
3819+ ready=$( kubectl get reaperdaemonjob test-simple-djob -o jsonpath=' {.status.readyNodes}' 2> /dev/null || echo " 0" )
3820+ total=$( kubectl get reaperdaemonjob test-simple-djob -o jsonpath=' {.status.totalNodes}' 2> /dev/null || echo " 0" )
3821+ [[ " $ready " -eq " $total " && " $total " -ge 1 ]] || {
3822+ log_error " Expected readyNodes == totalNodes >= 1, got ready=$ready total=$total "
3823+ return 1
3824+ }
3825+ log_verbose " ReaperDaemonJob completed: $ready /$total nodes"
3826+ }
3827+
3828+ test_daemon_job_node_statuses () {
3829+ # Verify per-node status entries exist with Succeeded phase
3830+ local node_count
3831+ node_count=$( kubectl get reaperdaemonjob test-simple-djob \
3832+ -o jsonpath=' {.status.nodeStatuses}' 2> /dev/null | python3 -c " import sys,json; print(len(json.loads(sys.stdin.read())))" 2> /dev/null || echo " 0" )
3833+ [[ " $node_count " -ge 1 ]] || {
3834+ log_error " Expected at least 1 nodeStatus entry, got $node_count "
3835+ return 1
3836+ }
3837+
3838+ # Check first node has Succeeded phase
3839+ local node_phase
3840+ node_phase=$( kubectl get reaperdaemonjob test-simple-djob \
3841+ -o jsonpath=' {.status.nodeStatuses[0].phase}' 2> /dev/null || true)
3842+ [[ " $node_phase " == " Succeeded" ]] || {
3843+ log_error " Expected first node phase=Succeeded, got '$node_phase '"
3844+ return 1
3845+ }
3846+ }
3847+
3848+ test_daemon_job_kubectl_columns () {
3849+ # Verify custom printer columns
3850+ local output
3851+ output=$( kubectl get reaperdaemonjobs 2>&1 || true)
3852+ echo " $output " | grep -qi " PHASE" || {
3853+ log_error " Missing PHASE column in kubectl get reaperdaemonjobs output"
3854+ return 1
3855+ }
3856+ echo " $output " | grep -qi " READY" || {
3857+ log_error " Missing READY column in kubectl get reaperdaemonjobs output"
3858+ return 1
3859+ }
3860+ echo " $output " | grep -qi " TOTAL" || {
3861+ log_error " Missing TOTAL column in kubectl get reaperdaemonjobs output"
3862+ return 1
3863+ }
3864+ }
3865+
3866+ test_daemon_job_dependency_ordering () {
3867+ # Create two DaemonJobs where the second depends on the first
3868+ kubectl apply -f - << 'YAML '
3869+ apiVersion: reaper.io/v1alpha1
3870+ kind: ReaperDaemonJob
3871+ metadata:
3872+ name: test-djob-dep-first
3873+ spec:
3874+ command: ["/bin/sh", "-c"]
3875+ args: ["echo step-1-done"]
3876+ ---
3877+ apiVersion: reaper.io/v1alpha1
3878+ kind: ReaperDaemonJob
3879+ metadata:
3880+ name: test-djob-dep-second
3881+ spec:
3882+ command: ["/bin/sh", "-c"]
3883+ args: ["echo step-2-done"]
3884+ after:
3885+ - test-djob-dep-first
3886+ YAML
3887+
3888+ # Wait for the first to complete
3889+ local phase=" "
3890+ for i in $( seq 1 60) ; do
3891+ phase=$( kubectl get reaperdaemonjob test-djob-dep-first -o jsonpath=' {.status.phase}' 2> /dev/null || true)
3892+ if [[ " $phase " == " Completed" ]]; then
3893+ break
3894+ fi
3895+ sleep 2
3896+ done
3897+ [[ " $phase " == " Completed" ]] || {
3898+ log_error " Dependency job test-djob-dep-first did not complete, phase='$phase '"
3899+ return 1
3900+ }
3901+
3902+ # Now the second should eventually complete too
3903+ for i in $( seq 1 60) ; do
3904+ phase=$( kubectl get reaperdaemonjob test-djob-dep-second -o jsonpath=' {.status.phase}' 2> /dev/null || true)
3905+ if [[ " $phase " == " Completed" ]]; then
3906+ break
3907+ fi
3908+ sleep 2
3909+ done
3910+ [[ " $phase " == " Completed" ]] || {
3911+ log_error " Dependent job test-djob-dep-second did not complete, phase='$phase '"
3912+ return 1
3913+ }
3914+ log_verbose " Dependency ordering works: first completed, then second completed"
3915+ }
3916+
3917+ test_daemon_job_gc_on_delete () {
3918+ # Verify that deleting a ReaperDaemonJob garbage collects its ReaperPods
3919+ local rp_count
3920+ rp_count=$( kubectl get reaperpods -l reaper.io/daemon-job=test-simple-djob --no-headers 2> /dev/null | wc -l | tr -d ' ' || echo " 0" )
3921+ [[ " $rp_count " -ge 1 ]] || {
3922+ log_error " Expected at least 1 ReaperPod before deletion"
3923+ return 1
3924+ }
3925+
3926+ kubectl delete reaperdaemonjob test-simple-djob >> " $LOG_FILE " 2>&1
3927+
3928+ # Wait for ReaperPods to be garbage collected
3929+ for i in $( seq 1 30) ; do
3930+ rp_count=$( kubectl get reaperpods -l reaper.io/daemon-job=test-simple-djob --no-headers 2> /dev/null | wc -l | tr -d ' ' || echo " 0" )
3931+ if [[ " $rp_count " -eq 0 ]]; then
3932+ break
3933+ fi
3934+ sleep 1
3935+ done
3936+ [[ " $rp_count " -eq 0 ]] || {
3937+ log_error " ReaperPods for test-simple-djob still exist after deletion ($rp_count remaining)"
3938+ return 1
3939+ }
3940+ }
3941+
3942+ cleanup_daemon_jobs () {
3943+ kubectl delete reaperdaemonjob --all --ignore-not-found >> " $LOG_FILE " 2>&1 || true
3944+ kubectl delete reaperpod -l reaper.io/daemon-job --ignore-not-found >> " $LOG_FILE " 2>&1 || true
3945+ # Wait for pods to terminate
3946+ for i in $( seq 1 15) ; do
3947+ local remaining
3948+ remaining=$( kubectl get reaperpods -l reaper.io/daemon-job --no-headers 2> /dev/null | wc -l | tr -d ' ' || echo " 0" )
3949+ if [[ " $remaining " -eq 0 ]]; then
3950+ break
3951+ fi
3952+ sleep 1
3953+ done
3954+ }
3955+
3956+ phase_daemon_job_tests () {
3957+ log_status " "
3958+ log_status " ${CLR_PHASE} Phase 4d: ReaperDaemonJob CRD tests${CLR_RESET} "
3959+ log_status " ========================================"
3960+
3961+ run_test test_daemon_job_crd_install " ReaperDaemonJob CRD installation" --hard-fail
3962+ run_test test_daemon_job_simple " Simple ReaperDaemonJob creates ReaperPods" --hard-fail
3963+ run_test test_daemon_job_status_tracking " ReaperDaemonJob status tracking" --hard-fail
3964+ run_test test_daemon_job_node_statuses " Per-node status entries" --hard-fail
3965+ run_test test_daemon_job_kubectl_columns " kubectl get reaperdaemonjobs columns" --hard-fail
3966+ run_test test_daemon_job_dependency_ordering " Dependency ordering (after)" --hard-fail
3967+ run_test test_daemon_job_gc_on_delete " GC ReaperPods on DaemonJob delete" --hard-fail
3968+
3969+ # Cleanup daemon job resources, then controller (deferred from Phase 4b)
3970+ cleanup_daemon_jobs
37423971 cleanup_controller
37433972}
37443973
0 commit comments