diff --git a/charts/cron-job/Chart.yaml b/charts/cron-job/Chart.yaml index 13369c1..9416c78 100644 --- a/charts/cron-job/Chart.yaml +++ b/charts/cron-job/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 description: EcoVadis Helm chart for K8s Cron Job name: charts-cron-job type: application -version: 2.10.0 +version: 2.11.0 dependencies: - name: charts-core version: 2.4.2 diff --git a/charts/cron-job/templates/helmtests/helm-test-rbac.yaml b/charts/cron-job/templates/helmtests/helm-test-rbac.yaml new file mode 100644 index 0000000..b91ed7d --- /dev/null +++ b/charts/cron-job/templates/helmtests/helm-test-rbac.yaml @@ -0,0 +1,61 @@ +{{- if .Values.global.helmTest.enabled }} + +{{- $cronjobs := $.Values.cronjobs }} +{{- if not $cronjobs }} + {{- $cronjobs = list (dict) }} +{{- end }} + +{{- range $index, $cronjob := $cronjobs }} +{{- $cronjobName := include "charts-cron-job.fullname" (dict "root" $ "cronjob" $cronjob "index" $index) }} +{{- $saName := printf "%s-helm-test" $cronjobName | trunc 63 | trimSuffix "-" }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ $saName }} + namespace: {{ $.Release.Namespace }} + labels: + {{- include "charts-cron-job.labels" (dict "root" $ "cronjob" $cronjob) | nindent 4 }} + annotations: + helm.sh/hook: test + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ $saName }} + namespace: {{ $.Release.Namespace }} + labels: + {{- include "charts-cron-job.labels" (dict "root" $ "cronjob" $cronjob) | nindent 4 }} + annotations: + helm.sh/hook: test + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded +rules: + - apiGroups: ["batch"] + resources: ["cronjobs"] + verbs: ["get"] + - apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["get", "list", "watch", "create", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ $saName }} + namespace: {{ $.Release.Namespace }} + labels: + {{- include "charts-cron-job.labels" (dict "root" $ "cronjob" $cronjob) | nindent 4 }} + annotations: + helm.sh/hook: test + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ $saName }} +subjects: + - kind: ServiceAccount + name: {{ $saName }} + namespace: {{ $.Release.Namespace }} +{{- end }} + +{{- end }} diff --git a/charts/cron-job/templates/helmtests/helm-test.yaml b/charts/cron-job/templates/helmtests/helm-test.yaml new file mode 100644 index 0000000..174525d --- /dev/null +++ b/charts/cron-job/templates/helmtests/helm-test.yaml @@ -0,0 +1,124 @@ +{{- if .Values.global.helmTest.enabled }} + +{{- $cronjobs := $.Values.cronjobs }} +{{- if not $cronjobs }} + {{- $cronjobs = list (dict) }} +{{- end }} + +{{- range $index, $cronjob := $cronjobs }} +{{- $global := $.Values.global }} +{{- $concurrencyPolicy := $cronjob.concurrencyPolicy | default $global.concurrencyPolicy }} +{{- $activeDeadlineSeconds := $cronjob.activeDeadlineSeconds | default $global.activeDeadlineSeconds }} +{{- $cronjobName := include "charts-cron-job.fullname" (dict "root" $ "cronjob" $cronjob "index" $index) }} +{{- $saName := printf "%s-helm-test" $cronjobName | trunc 63 | trimSuffix "-" }} +{{- $executorName := printf "%s-helm-test-executor" $cronjobName | trunc 63 | trimSuffix "-" }} +{{- /* Spawned test Job name base: truncated to 46 chars so the runtime suffix + "-<10-digit-epoch>-<5-digit-random>" (17 chars max) fits within the + 63-char Kubernetes resource name limit. */ -}} +{{- $jobNameBase := printf "%s-test" $cronjobName | trunc 46 | trimSuffix "-" }} +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ $executorName }} + namespace: {{ $.Release.Namespace }} + labels: + {{- include "charts-cron-job.labels" (dict "root" $ "cronjob" $cronjob) | nindent 4 }} + annotations: + helm.sh/hook: test + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded +spec: + backoffLimit: 1 + template: + metadata: + labels: + {{- include "charts-cron-job.labels" (dict "root" $ "cronjob" $cronjob) | nindent 8 }} + spec: + serviceAccountName: {{ $saName }} + restartPolicy: Never + containers: + - name: helm-test + image: {{ $.Values.global.image.repository }}/{{ $.Values.global.helmTest.image }} + command: + - /bin/sh + - -c + - | + set -e + + CRONJOB="{{ $cronjobName }}" + NAMESPACE="{{ $.Release.Namespace }}" + POLICY="{{ $concurrencyPolicy }}" + TIMEOUT="{{ $activeDeadlineSeconds }}" + + # Unique suffix: epoch seconds + shell $RANDOM (0-32767). + # Base is pre-truncated to 46 chars at render time so the full name + # stays within the 63-char Kubernetes resource name limit. + JOB_NAME="{{ $jobNameBase }}-$(date +%s)-${RANDOM}" + + echo "==> Helm test for CronJob: ${CRONJOB}" + echo " ConcurrencyPolicy : ${POLICY}" + echo " Test Job name : ${JOB_NAME}" + echo " Timeout : ${TIMEOUT}s" + + # ── Forbid: wait for any currently-active job to finish ────────── + if [ "$POLICY" = "Forbid" ]; then + echo "==> ConcurrencyPolicy=Forbid: waiting for active jobs to finish before creating test job..." + + # Resolve the CronJob UID at runtime. Jobs spawned by this CronJob + # carry an ownerReference pointing to this UID — the authoritative + # way to identify all jobs that belong to a specific CronJob instance + # (as seen in the job's .metadata.ownerReferences[].uid field). + CRONJOB_UID=$(kubectl get cronjob "${CRONJOB}" -n "${NAMESPACE}" \ + -o jsonpath="{.metadata.uid}") + echo " CronJob UID: ${CRONJOB_UID}" + + ELAPSED=0 + while true; do + # Emit " " for every job, then keep only + # lines where the ownerUID matches this CronJob and activeCount > 0. + ACTIVE=$(kubectl get jobs -n "${NAMESPACE}" \ + -o jsonpath="{range .items[*]}{.metadata.ownerReferences[0].uid}{' '}{.status.active}{'\n'}{end}" \ + | awk -v uid="${CRONJOB_UID}" \ + "NF==2 && \$1==uid && \$2+0>0 {count++} END {print count+0}") + if [ "${ACTIVE}" -eq 0 ]; then + echo " No active jobs found. Proceeding." + break + fi + if [ "${ELAPSED}" -ge "${TIMEOUT}" ]; then + echo "ERROR: Timed out after ${TIMEOUT}s waiting for active jobs to finish." + exit 1 + fi + echo " ${ACTIVE} active job(s) still running. Waiting 10s... (${ELAPSED}s elapsed)" + sleep 10 + ELAPSED=$((ELAPSED + 10)) + done + + # ── Replace: delete any active scheduler-spawned jobs first ────── + elif [ "$POLICY" = "Replace" ]; then + echo "==> ConcurrencyPolicy=Replace: deleting active jobs owned by this CronJob..." + CRONJOB_UID=$(kubectl get cronjob "${CRONJOB}" -n "${NAMESPACE}" \ + -o jsonpath="{.metadata.uid}") + # Find names of all jobs whose ownerReference points to this CronJob + # and that currently have active pods, then delete them. + kubectl get jobs -n "${NAMESPACE}" \ + -o jsonpath="{range .items[*]}{.metadata.ownerReferences[0].uid}{' '}{.status.active}{' '}{.metadata.name}{'\n'}{end}" \ + | awk -v uid="${CRONJOB_UID}" "NF==3 && \$1==uid && \$2+0>0 {print \$3}" \ + | xargs -r kubectl delete job -n "${NAMESPACE}" --ignore-not-found + echo " Done." + fi + + # ── Create the test job from the CronJob ───────────────────────── + echo "==> Creating test job '${JOB_NAME}' from CronJob '${CRONJOB}'..." + kubectl create job "${JOB_NAME}" --from=cronjob/"${CRONJOB}" -n "${NAMESPACE}" + + # ── Wait for completion ─────────────────────────────────────────── + echo "==> Waiting for job '${JOB_NAME}' to complete (timeout: ${TIMEOUT}s)..." + kubectl wait job/"${JOB_NAME}" \ + -n "${NAMESPACE}" \ + --for=condition=complete \ + --timeout="${TIMEOUT}s" + + echo "==> Test job completed successfully." +{{- end }} + +{{- end }} diff --git a/charts/cron-job/values.yaml b/charts/cron-job/values.yaml index 846d632..85d5153 100644 --- a/charts/cron-job/values.yaml +++ b/charts/cron-job/values.yaml @@ -156,4 +156,8 @@ global: enabled: false canary: - enabled: false \ No newline at end of file + enabled: false + + helmTest: + enabled: true + image: dockerhub/alpine/kubectl:latest # Image used by the helm test pod; must contain kubectl \ No newline at end of file