-
Notifications
You must be signed in to change notification settings - Fork 0
feat(observability): alert on hermes cron and kopiur backup health #4302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,4 +5,5 @@ kind: Kustomization | |
|
|
||
| resources: | ||
| - helmrelease.yaml | ||
| - prometheusrule.yaml | ||
| - secret.sops.yaml | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| --- | ||
| # yaml-language-server: $schema=https://k8s-schemas.home-operations.com/monitoring.coreos.com/prometheusrule_v1.json | ||
| apiVersion: monitoring.coreos.com/v1 | ||
| kind: PrometheusRule | ||
| metadata: | ||
| name: hermes-rules | ||
| spec: | ||
| groups: | ||
| - name: hermes.rules | ||
| # scripts/cron_health_export.py (on the hermes PVC, not in git) pushes | ||
| # cron/jobs.json state every 10m. Pushed, not scraped, so VM's 5m default | ||
| # staleness makes a bare instant selector return nothing between pushes: | ||
| # absent() would flap and the failure rules would go blind exactly half the | ||
| # time. last_over_time() pins every rule to the newest real sample instead of | ||
| # the staleness window. | ||
| rules: | ||
| # The scheduler already records per-job status in cron/jobs.json; nothing read | ||
| # it, so a dead cron stayed dark 40h. | ||
| - alert: HermesCronJobFailed | ||
| expr: |- | ||
| last_over_time(hermes_cron_last_status_ok[30m]) == 0 | ||
| for: 15m | ||
| annotations: | ||
| summary: >- | ||
| Hermes cron {{ $labels.name }} last run failed — it will keep failing on | ||
| schedule until the cause is fixed | ||
| labels: | ||
| severity: warning | ||
|
|
||
| # Delivery is a separate failure from execution: the job can succeed and the | ||
| # Discord post still vanish, which reads green everywhere else. | ||
| - alert: HermesCronDeliveryFailed | ||
| expr: |- | ||
| last_over_time(hermes_cron_delivery_failed[30m]) == 1 | ||
| for: 15m | ||
| annotations: | ||
| summary: >- | ||
| Hermes cron {{ $labels.name }} ran but could not deliver its output — | ||
| the result is lost, not late | ||
| labels: | ||
| severity: warning | ||
|
|
||
| # Already excludes disabled and paused jobs at export time. One hour of slack | ||
| # absorbs a long-running predecessor without flapping. | ||
| - alert: HermesCronOverdue | ||
| expr: |- | ||
| last_over_time(hermes_cron_overdue_seconds[30m]) > 3600 | ||
| for: 15m | ||
| annotations: | ||
| summary: >- | ||
| Hermes cron {{ $labels.name }} is {{ $value | humanizeDuration }} past its | ||
| scheduled run — the scheduler is not firing it | ||
| labels: | ||
| severity: warning | ||
|
|
||
| # The exporter runs inside hermes, so a stale heartbeat means hermes itself is | ||
| # down. Without this the other three rules go quiet exactly when they matter. | ||
| - alert: HermesCronExporterStale | ||
| expr: |- | ||
| time() - last_over_time(hermes_cron_export_timestamp[2h]) > 1800 | ||
| for: 5m | ||
| annotations: | ||
| summary: >- | ||
| Hermes cron health metrics are {{ $value | humanizeDuration }} stale — | ||
| cron alerting is blind, check the hermes pod | ||
| labels: | ||
| severity: warning | ||
|
|
||
| # No for:: absent() over a 2h-integrated window cannot flap, and any delay here | ||
| # reopens the gap where Stale has already resolved but Absent has not yet fired, | ||
| # which reports "recovered" in the middle of an outage. | ||
| - alert: HermesCronExporterAbsent | ||
| expr: |- | ||
| absent(last_over_time(hermes_cron_export_timestamp[2h])) | ||
| annotations: | ||
| summary: >- | ||
| No Hermes cron health metrics at all — the exporter never ran or was removed | ||
| labels: | ||
| severity: warning |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,5 +10,10 @@ subjects: | |
| apiGroup: rbac.authorization.k8s.io | ||
| roleRef: | ||
| kind: ClusterRole | ||
| name: flux-web-admin | ||
| # The web UI authenticates as Anonymous, so this role is granted to anyone who can | ||
| # reach envoy-internal. flux-web-admin adds create on batch/jobs (arbitrary pod | ||
| # execution), patch/restart on workloads and delete on pods; flux-web-user is the | ||
| # chart's read-only variant. It still carries get/list/watch on *, which includes | ||
| # Secrets -- only real auth or dropping web.httpRoute closes that. | ||
| name: flux-web-user | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
kubectl get clusterrolebinding flux-admin \
-o jsonpath='{.roleRef.name}{"\n"}'
kubectl apply --server-side --dry-run=server \
-f kubernetes/apps/flux-system/flux-operator/app/clusterrolebinding.yamlRepository: Tanguille/cluster Length of output: 200 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- target manifest ---'
cat -n kubernetes/apps/flux-system/flux-operator/app/clusterrolebinding.yaml
printf '%s\n' '--- related Flux operator manifests ---'
rg -n -C 5 'flux-admin|flux-web-admin|flux-web-user|roleRef|ClusterRoleBinding|anonymous|flux-admin' \
kubernetes/apps/flux-system/flux-operator kubernetes 2>/dev/null || true
printf '%s\n' '--- target history summary ---'
git log --oneline -8 -- kubernetes/apps/flux-system/flux-operator/app/clusterrolebinding.yamlRepository: Tanguille/cluster Length of output: 13120 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- target manifest history and parent state ---'
git show --format=fuller --stat 65e51b09 -- kubernetes/apps/flux-system/flux-operator/app/clusterrolebinding.yaml
git show 65e51b09^:kubernetes/apps/flux-system/flux-operator/app/clusterrolebinding.yaml 2>/dev/null | cat -n || true
git show 65e51b09:kubernetes/apps/flux-system/flux-operator/app/clusterrolebinding.yaml | cat -n
printf '%s\n' '--- Flux Kustomization and app files ---'
fd -t f . kubernetes/apps/flux-system/flux-operator | sort
rg -n -C 6 'kind: Kustomization|prune:|clusterrolebinding.yaml|flux-operator' \
kubernetes/apps/flux-system/flux-operator
printf '%s\n' '--- all repository references to the binding and roles ---'
rg -n -C 4 'flux-admin|flux-web-admin|flux-web-user' . --glob '!*.lock' || true
printf '%s\n' '--- recent commits affecting the Flux RBAC transition ---'
git log --oneline --all -- kubernetes/apps/flux-system/flux-operatorRepository: Tanguille/cluster Length of output: 11311 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- working-tree change under review ---'
git status --short
git diff -- kubernetes/apps/flux-system/flux-operator/app/clusterrolebinding.yaml \
kubernetes/apps/flux-system/flux-operator/app/helmrelease.yaml \
kubernetes/apps/flux-system/flux-operator/ks.yaml
printf '%s\n' '--- exact prior binding introduction/change ---'
git log --oneline -S'flux-web-admin' -- kubernetes/apps/flux-system/flux-operator/app/clusterrolebinding.yaml
git log --oneline -S'flux-web-user' -- kubernetes/apps/flux-system/flux-operator/app/clusterrolebinding.yaml
git show --format=fuller --stat 1981a310 -- kubernetes/apps/flux-system/flux-operator/app/clusterrolebinding.yaml kubernetes/apps/flux-system/flux-operator/app/helmrelease.yaml
git show 1981a310 -- kubernetes/apps/flux-system/flux-operator/app/clusterrolebinding.yaml kubernetes/apps/flux-system/flux-operator/app/helmrelease.yaml
printf '%s\n' '--- Flux Kustomization controller settings and prune documentation references ---'
rg -n -C 3 'server-side|force|prune|inventory|delet(e|ion)' \
kubernetes flux-system .github 2>/dev/null | head -250 || trueRepository: Tanguille/cluster Length of output: 18963 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- current revision and target-file commits ---'
git rev-parse HEAD
git log --format='%h %ad %s' --date=short -12 -- kubernetes/apps/flux-system/flux-operator/app/clusterrolebinding.yaml
git log --format='%h %ad %s' --date=short -12 -S'flux-web-user' -- kubernetes/apps/flux-system/flux-operator/app/clusterrolebinding.yaml
printf '%s\n' '--- commits that contain the current role reference ---'
for c in $(git log --format='%H' -S'flux-web-user' -- kubernetes/apps/flux-system/flux-operator/app/clusterrolebinding.yaml | head -5); do
echo "commit $c"
git show --format='%h %s' --no-ext-diff "$c" -- kubernetes/apps/flux-system/flux-operator/app/clusterrolebinding.yaml | sed -n '1,100p'
done
printf '%s\n' '--- Flux Kustomization resource and controller version context ---'
cat -n kubernetes/apps/flux-system/flux-operator/ks.yaml
cat -n kubernetes/flux/cluster/ks.yaml | sed -n '1,80p'
rg -n 'fluxcd|kustomize-controller|flux-system' kubernetes/apps/flux-system kubernetes/flux | head -120Repository: Tanguille/cluster Length of output: 12316 🌐 Web query:
💡 Result: When you rename a resource managed by FluxCD—specifically if you mean renaming the Kustomization object itself—the behavior depends on your prune configuration [1][2]. If spec.prune is set to true, renaming the Kustomization object will cause Flux to treat the original Kustomization as deleted, which triggers the garbage collection (pruning) of all resources previously managed by that Kustomization [1][3][2]. Consequently, those resources will be deleted from the cluster before the new Kustomization (with the new name) attempts to create them [2]. To rename a Flux Kustomization without triggering this deletion, you must perform the following steps [1][2]: 1. Disable garbage collection by setting spec.prune: false in your Kustomization manifest [1]. 2. Commit and push this change to your repository, then wait for Flux to reconcile and acknowledge the change (you can verify this using flux export kustomization) [1]. 3. Rename the Kustomization object in your manifest, then commit and push the change [1]. 4. Flux will delete the old Kustomization object but will not delete the associated workloads because pruning was disabled [1]. 5. Once the new Kustomization is active, you can re-enable pruning by setting spec.prune: true [1]. Flux will then adopt ownership of the existing resources [1]. If you are referring to renaming a specific resource within the manifests being synced by a Kustomization (rather than the Kustomization object itself), Flux will perceive this as a deletion of the old resource and an addition of a new one [3]. Because the old resource is missing from the source revision, Flux will prune it immediately if pruning is enabled [3]. There is no native behavior to ensure the new resource is created before the old one is deleted; the deletion of orphaned resources happens as part of the reconciliation process [3]. To move resources safely between different Kustomizations (or rename them in a way that requires avoiding deletion), the best practice is to temporarily disable pruning while the migration occurs [1]. Citations:
Recreate the binding during the role transition. The 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| apiGroup: rbac.authorization.k8s.io | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,3 +5,4 @@ kind: Kustomization | |
| resources: | ||
| - helmrelease.yaml | ||
| - ocirepository.yaml | ||
| - prometheusrule.yaml | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| --- | ||
| # yaml-language-server: $schema=https://k8s-schemas.home-operations.com/monitoring.coreos.com/prometheusrule_v1.json | ||
| apiVersion: monitoring.coreos.com/v1 | ||
| kind: PrometheusRule | ||
| metadata: | ||
| name: kopiur-rules | ||
| spec: | ||
| groups: | ||
| # The chart ships its own `kopiur.rules` group (monitoring.prometheusRule.enabled | ||
| # in the HelmRelease); group and alert names here must not collide with it. | ||
| - name: kopiur.freshness.rules | ||
| rules: | ||
| # Backups were down 44h in July before anyone noticed: nothing watched | ||
| # freshness. Snapshots run hourly, so 6h is 6 missed runs, not a slow one. | ||
| # Named Overdue, not Stale: the chart's KopiurBackupStale fires off the same | ||
| # metric with the same severity, so a shared name is one alert fingerprint. | ||
| - alert: KopiurBackupOverdue | ||
| expr: |- | ||
| time() - kopiur_policy_last_backup_success_timestamp_seconds > 6 * 60 * 60 | ||
| for: 30m | ||
|
Comment on lines
+18
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- target file ---'
cat -n kubernetes/apps/kopiur-system/kopiur/app/prometheusrule.yaml
printf '%s\n' '--- Kopiur-related files ---'
git ls-files | rg -i 'kopiur|snapshot.?policy|prometheus.?rule|servicemonitor|vmrule|prometheus'
printf '%s\n' '--- Kopiur references ---'
rg -n -i 'kopiur_policy_last_backup_success_timestamp_seconds|kopiur_repository|kopiur.*leader|SnapshotPolicy|prometheusRule|ruleSelector|vmrule|prometheusrule' kubernetes --glob '*.yaml' --glob '*.yml' || trueRepository: Tanguille/cluster Length of output: 14744 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- target file ---'
cat -n kubernetes/apps/kopiur-system/kopiur/app/prometheusrule.yaml
printf '%s\n' '--- Kopiur-related files ---'
git ls-files | rg -i 'kopiur|snapshot.?policy|prometheus.?rule|servicemonitor|vmrule|prometheus'
printf '%s\n' '--- Kopiur references ---'
rg -n -i 'kopiur_policy_last_backup_success_timestamp_seconds|kopiur_repository|kopiur.*leader|SnapshotPolicy|prometheusRule|ruleSelector|vmrule|prometheusrule' kubernetes --glob '*.yaml' --glob '*.yml' || trueRepository: Tanguille/cluster Length of output: 14744 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- SnapshotPolicy component ---'
cat -n kubernetes/components/kopiur/backup/snapshotpolicy.yaml
cat -n kubernetes/components/kopiur/backup/kustomization.yaml
cat -n kubernetes/components/kopiur/kustomization.yaml
printf '%s\n' '--- Kopiur application configuration ---'
cat -n kubernetes/apps/kopiur-system/kopiur/app/helmrelease.yaml
cat -n kubernetes/apps/kopiur-system/kopiur/app/kustomization.yaml
cat -n kubernetes/apps/kopiur-system/kopiur/ks.yaml
printf '%s\n' '--- monitoring selectors and PrometheusRule patterns ---'
rg -n -i 'ruleSelector|ruleNamespaceSelector|prometheus:|release:|monitoring.coreos.com/v1|kind: PrometheusRule' kubernetes --glob '*.yaml' --glob '*.yml' | head -250
printf '%s\n' '--- all SnapshotPolicy declarations ---'
rg -n -C 12 'kind: SnapshotPolicy' kubernetes --glob '*.yaml' --glob '*.yml' || trueRepository: Tanguille/cluster Length of output: 14538 🏁 Script executed: #!/bin/bash
set -eu
rg -n -i 'kopiur|snapshotpolicy|prometheusrule|ruleSelector|ruleNamespaceSelector' . --glob '*.yaml' --glob '*.yml' --glob '*.md' --glob '*.json' || trueRepository: Tanguille/LLMKube Length of output: 11859 🌐 Web query:
💡 Result: The metric Citations:
🌐 Web query:
💡 Result: The term kopiur_policy_last_backup_success_timestamp_seconds is a Prometheus metric exported by Kopiur [1], a Kopia-native Kubernetes backup operator [2][3]. This metric tracks the Unix timestamp (in seconds) of the last successful backup operation associated with a Kopiur SnapshotPolicy [1]. It is primarily used within Prometheus alerting rules to monitor backup freshness. For example, it is often compared against a threshold (such as backupStaleAfterSeconds, which defaults to 172,800 seconds or 48 hours) to determine if a backup policy is considered stale [1]. Kopiur itself is an open-source project under the Home Operations organization, designed to manage Kopia repositories and snapshots as first-class Kubernetes custom resources (CRDs) [2][3][4]. Citations:
🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Kopiur component consumers ---'
rg -n -C 8 'kopiur/backup|components:|SnapshotSchedule|BACKUP_SNAPSHOTCLASS|KOPIUR_PUID' kubernetes/apps kubernetes/components --glob '*.yaml' --glob '*.yml' | head -400
printf '%s\n' '--- Victoria Metrics and kube-state-metrics configuration ---'
cat -n kubernetes/apps/observability/victoria-metrics/app/helmrelease.yaml | sed -n '1,380p'
fd -i 'helmrelease.yaml' kubernetes/apps/observability/kube-state-metrics kubernetes/apps/observability 2>/dev/null | xargs -r -n1 sh -c 'echo "--- $0"; cat -n "$0"'
rg -n -i 'customResourceState|customresources|kube_customresource|kube-state-metrics|ruleSelector|ruleNamespaceSelector|Prometheus' kubernetes/apps/observability kubernetes/config --glob '*.yaml' --glob '*.yml' || trueRepository: Tanguille/cluster Length of output: 50376 🏁 Script executed (no clone): Length of output: 10915 🏁 Script executed (no clone): Length of output: 271 🏁 Script executed (no clone): Length of output: 174 🏁 Script executed (no clone): Length of output: 18617 🏁 Script executed (no clone): Length of output: 282 Add per-policy missing-series detection. 🤖 Prompt for AI Agents |
||
| annotations: | ||
| summary: >- | ||
| kopiur {{ $labels.namespace }}/{{ $labels.policy }} has no successful backup | ||
| for {{ $value | humanizeDuration }} — restore point is aging | ||
| labels: | ||
| severity: warning | ||
|
|
||
| - alert: KopiurBackupCritical | ||
| expr: |- | ||
| time() - kopiur_policy_last_backup_success_timestamp_seconds > 24 * 60 * 60 | ||
| for: 30m | ||
| annotations: | ||
| summary: >- | ||
| kopiur {{ $labels.namespace }}/{{ $labels.policy }} has not backed up | ||
| successfully in {{ $value | humanizeDuration }} | ||
| labels: | ||
| severity: critical | ||
|
|
||
| # The July outage removed the CSI snapshot CRDs, so policies stopped reporting | ||
| # rather than reporting failure. Absence is the failure mode to catch. | ||
| - alert: KopiurPolicyMetricsAbsent | ||
| expr: |- | ||
| absent(kopiur_policy_last_backup_success_timestamp_seconds) | ||
| for: 30m | ||
| annotations: | ||
| summary: >- | ||
| No kopiur policy backup metrics at all — the controller is down or every | ||
| policy stopped reporting; backup state is unknown, not healthy | ||
| labels: | ||
| severity: critical | ||
|
|
||
| # A repo whose snapshot count only ever grows means retention is not pruning; | ||
| # a drop to zero means the repo was emptied under us. | ||
| - alert: KopiurRepoEmpty | ||
| expr: |- | ||
| kopiur_repo_snapshot_count == 0 | ||
| for: 1h | ||
| annotations: | ||
| summary: >- | ||
| kopiur repository {{ $labels.name }} reports zero snapshots — the repo is | ||
| empty or unreadable | ||
| labels: | ||
| severity: critical | ||
|
|
||
| - alert: KopiurControllerNotLeader | ||
| expr: |- | ||
| max(kopiur_leader_is_leader) == 0 | ||
| for: 30m | ||
| annotations: | ||
| summary: >- | ||
| No kopiur controller holds the leader lease — nothing is scheduling backups | ||
| labels: | ||
| severity: critical | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: Tanguille/cluster
Length of output: 193
🏁 Script executed:
Repository: Tanguille/cluster
Length of output: 50373
🌐 Web query:
Flux Operator Helm chart 0.57.0 flux-web-user networkPolicy namespaceSelector web RBAC anonymous UI💡 Result:
In Flux Operator Helm chart 0.57.0, the web interface configuration and security features interact as follows: Web UI and RBAC The Flux Web UI uses Kubernetes RBAC for user authorization [1]. By default, the operator includes the flux-web-user ClusterRole, which provides read-only access to Flux resources and workloads [1][2]. For environments requiring custom, least-privilege access, administrators can bind more restrictive roles to users or groups [2][3]. Anonymous Authentication The UI supports an Anonymous authentication mode [4]. When enabled via the Web Config API (web.config), all users share a fixed identity (username and groups) defined in the configuration [1][4]. Because this identity is shared by all users, this mode is intended only for secure environments with trusted users [4]. Network Policy and Namespace Selection - NetworkPolicy: The Helm chart includes a web.networkPolicy object that defaults to create: true to secure access to the web interface [5][6][7]. - Namespace Filtering: The UI uses the web server's service account to internally list all namespaces and then applies a SelfSubjectAccessReview for the current user to filter the list [8]. Users only see namespaces in the UI for which they have get permissions on the ResourceSet custom resource [1][2][8]. This design prevents the need for cluster-wide namespace list permissions [8]. Helm Chart Configuration (web.serverOnly) When deploying the Web UI as a standalone service (web.serverOnly: true), it acts as a dedicated Helm release separate from the Flux Operator [5][6]. In this mode, if web.userActions.access is set to FineGrained, the web server performs actions using its own service account privileges rather than impersonating the user [6][7]. References: [5][6][7] Official Helm chart documentation for configuration values (web.enabled, web.networkPolicy, etc.). [1][4][8] Flux Operator Web UI documentation on User Management, Web Config API, and RBAC least privilege. [2] Documentation on the flux-web-user ClusterRole. [3] GitHub issue
#760discussing minimal RBAC for the Web UI.Citations:
🏁 Script executed (no clone):
Length of output: 4199
🏁 Script executed (no clone):
Length of output: 9506
🏁 Script executed (no clone):
Length of output: 30810
🏁 Script executed (no clone):
Length of output: 11945
🏁 Script executed (no clone):
Length of output: 176
Restrict anonymous Flux web access.
Every visitor receives the
flux-adminidentity, which is bound toflux-web-userwith cluster-wideget,list, andwatchpermissions. The NetworkPolicy also permits pods from every namespace to reach port 9080. The UI does not return Secret or ConfigMap data, but this still exposes all other UI-readable resources to unauthenticated callers.Use OAuth2/OIDC, or bind the anonymous identity to a custom least-privilege role. Restrict port 9080 to the Envoy source and port 8080 to the monitoring source. Apply these changes through the repository manifests and Flux reconciliation.
📍 Affects 2 files
kubernetes/apps/flux-system/flux-operator/app/clusterrolebinding.yaml#L13-L18(this comment)kubernetes/apps/flux-system/flux-operator/app/helmrelease.yaml#L30-L34🤖 Prompt for AI Agents
Source: Coding guidelines