diff --git a/Makefile b/Makefile index 763207b..801a054 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ KUBECONFIG ?= $(HOME)/.kube/config CLEANER_NAMESPACE ?= $(NAMESPACE) CLEANER_SCHEDULE ?= 0 * * * * -CLEANER_LABEL_SELECTOR ?= hyperfleet.io/cluster-id +CLEANER_LABEL_SELECTOR ?= hyperfleet.io/cluster-id hyperfleet.io/test-run CLEANER_AGE_MINUTES ?= 180 CLEANER_MAESTRO_URL ?= http://maestro.$(MAESTRO_NAMESPACE).svc.cluster.local:8000 @@ -211,7 +211,7 @@ install-repos: check-helmfile-env ## Add all hyperfleet helm repos $(call add-helm-repo,adapter,$(ADAPTER_CHART_REF)) .PHONY: install-hyperfleet -install-hyperfleet: check-helmfile-env ## Install all HyperFleet components +install-hyperfleet: check-helmfile-env check-hyperfleet-namespace ## Install all HyperFleet components helmfile -f helmfile/helmfile.yaml.gotmpl -e $(HELMFILE_ENV) apply .PHONY: install-api @@ -334,8 +334,12 @@ define check-namespace endef .PHONY: check-hyperfleet-namespace -check-hyperfleet-namespace: ## Create Hyperfleet namespace if it doesn't exist +check-hyperfleet-namespace: ## Create Hyperfleet namespace if it doesn't exist and label it + @printf '%s' "$(NAMESPACE)" | grep -qE '^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$$' \ + || { echo "ERROR: NAMESPACE '$(NAMESPACE)' is not a valid DNS label (lowercase alphanumeric and hyphens, 1-63 chars)"; exit 1; } $(call check-namespace,$(NAMESPACE)) + @kubectl label namespace "$(NAMESPACE)" "hyperfleet.io/test-run=$(NAMESPACE)" --overwrite >/dev/null + @echo "OK: namespace $(NAMESPACE) labeled with hyperfleet.io/test-run=$(NAMESPACE)" .PHONY: check-maestro-namespace check-maestro-namespace: ## Create Maestro namespace if it doesn't exist diff --git a/helm/namespace-cleaner/scripts/namespace-cleaner.sh b/helm/namespace-cleaner/scripts/namespace-cleaner.sh old mode 100644 new mode 100755 index 8042ad5..3fa775d --- a/helm/namespace-cleaner/scripts/namespace-cleaner.sh +++ b/helm/namespace-cleaner/scripts/namespace-cleaner.sh @@ -1,7 +1,7 @@ #!/bin/bash set -eo pipefail -LABEL_SELECTOR="${LABEL_SELECTOR:-hyperfleet.io/cluster-id}" +LABEL_SELECTOR="${LABEL_SELECTOR:-hyperfleet.io/cluster-id hyperfleet.io/test-run}" AGE_MINUTES="${AGE_MINUTES:-180}" MAESTRO_URL="${MAESTRO_URL:-http://maestro.maestro.svc.cluster.local:8000}" DRY_RUN="${DRY_RUN:-false}" @@ -72,26 +72,31 @@ else fi # --- Step 2: delete stale namespaces (non-blocking) --- -kubectl get namespaces -l "${LABEL_SELECTOR}" \ - -o go-template='{{range .items}}{{.metadata.name}}|{{.metadata.creationTimestamp}}|{{.status.phase}}{{"\n"}}{{end}}' \ -| while IFS='|' read -r ns_name created_at phase; do - [ -z "${ns_name}" ] && continue - [ "${phase}" != "Active" ] && continue +# LABEL_SELECTOR may contain multiple space-separated selectors; each is matched +# independently (OR semantics). Namespaces already Terminating are skipped. +IFS=' ' read -ra _selectors <<< "${LABEL_SELECTOR}" +for selector in "${_selectors[@]}"; do + kubectl get namespaces -l "${selector}" \ + -o go-template='{{range .items}}{{.metadata.name}}|{{.metadata.creationTimestamp}}|{{.status.phase}}{{"\n"}}{{end}}' \ + | while IFS='|' read -r ns_name created_at phase; do + [ -z "${ns_name}" ] && continue + [ "${phase}" != "Active" ] && continue - created_seconds=$(parse_timestamp "${created_at}") || continue - age=$((NOW - created_seconds)) + created_seconds=$(parse_timestamp "${created_at}") || continue + age=$((NOW - created_seconds)) - if [ "${age}" -gt "${AGE_SECONDS}" ]; then - age_m=$((age / 60)) - if [ "${DRY_RUN}" = "true" ]; then - echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [DRY-RUN] Would delete namespace '${ns_name}' (age=${age_m}m)" - else - echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [INFO] Deleting namespace '${ns_name}' (age=${age_m}m)" - kubectl delete namespace "${ns_name}" --wait=false \ - && echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [INFO] Delete requested for namespace '${ns_name}'" \ - || echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [WARN] Failed to delete namespace '${ns_name}'" + if [ "${age}" -gt "${AGE_SECONDS}" ]; then + age_m=$((age / 60)) + if [ "${DRY_RUN}" = "true" ]; then + echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [DRY-RUN] Would delete namespace '${ns_name}' (age=${age_m}m)" + else + echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [INFO] Deleting namespace '${ns_name}' (age=${age_m}m)" + kubectl delete namespace "${ns_name}" --wait=false \ + && echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [INFO] Delete requested for namespace '${ns_name}'" \ + || echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [WARN] Failed to delete namespace '${ns_name}'" + fi fi - fi - done + done +done echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [INFO] Namespace cleaner run complete" diff --git a/helm/namespace-cleaner/templates/_helpers.tpl b/helm/namespace-cleaner/templates/_helpers.tpl index 48a8cec..1fa6507 100644 --- a/helm/namespace-cleaner/templates/_helpers.tpl +++ b/helm/namespace-cleaner/templates/_helpers.tpl @@ -40,6 +40,14 @@ app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} +{{/* +Name for cluster-scoped resources (ClusterRole, ClusterRoleBinding). +Includes the release namespace so multiple installs don't collide. +*/}} +{{- define "namespace-cleaner.clusterResourceName" -}} +{{- printf "%s-%s" (include "namespace-cleaner.fullname" .) .Release.Namespace | trunc 63 | trimSuffix "-" }} +{{- end }} + {{/* Selector labels */}} diff --git a/helm/namespace-cleaner/templates/clusterrole.yaml b/helm/namespace-cleaner/templates/clusterrole.yaml index 9a5c68c..b975246 100644 --- a/helm/namespace-cleaner/templates/clusterrole.yaml +++ b/helm/namespace-cleaner/templates/clusterrole.yaml @@ -1,7 +1,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: {{ include "namespace-cleaner.fullname" . }} + name: {{ include "namespace-cleaner.clusterResourceName" . }} labels: {{- include "namespace-cleaner.labels" . | nindent 4 }} rules: diff --git a/helm/namespace-cleaner/templates/clusterrolebinding.yaml b/helm/namespace-cleaner/templates/clusterrolebinding.yaml index 20fb6db..1e9a3d6 100644 --- a/helm/namespace-cleaner/templates/clusterrolebinding.yaml +++ b/helm/namespace-cleaner/templates/clusterrolebinding.yaml @@ -1,13 +1,13 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - name: {{ include "namespace-cleaner.fullname" . }} + name: {{ include "namespace-cleaner.clusterResourceName" . }} labels: {{- include "namespace-cleaner.labels" . | nindent 4 }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: {{ include "namespace-cleaner.fullname" . }} + name: {{ include "namespace-cleaner.clusterResourceName" . }} subjects: - kind: ServiceAccount name: {{ include "namespace-cleaner.fullname" . }} diff --git a/helm/namespace-cleaner/values.yaml b/helm/namespace-cleaner/values.yaml index 3bb3158..1a04ec4 100644 --- a/helm/namespace-cleaner/values.yaml +++ b/helm/namespace-cleaner/values.yaml @@ -8,8 +8,9 @@ image: # Cron schedule for the cleaner job (default: every hour) schedule: "0 * * * *" -# Kubernetes label selector used to identify namespaces eligible for deletion -labelSelector: "hyperfleet.io/cluster-id" +# Space-separated label selectors used to identify namespaces eligible for deletion. +# Each selector is matched independently (OR semantics). +labelSelector: "hyperfleet.io/cluster-id hyperfleet.io/test-run" # Delete namespaces older than this many minutes ageMinutes: 180