prole/mock_val/init_monitoring.sh
chrisfu 3f96f66a78 feat(mock_val): rewrite init scripts; add new service init scripts
Rewrites (updated for knoe namespace, GKE support, and current service configs):
  init_gitlab.sh, init_kong.sh, init_cnpg_backup.sh, init_monitoring.sh,
  init_garage_store.sh, init_gitea.sh, init_kdc.sh, init_openbao.sh,
  init_argocd.sh, init_certmgr.sh, init_common_services.sh, init_db_manager.sh,
  init_forgejo.sh, init_k3s_registry.sh, init_kerberos.sh, init_nginx_ingress.sh,
  init_port_forwards.sh, init_registry.sh, init_service_layer.sh

Deleted: init_cloudnative_pg.sh (superseded by init_cnpg_gke.sh)

New scripts:
  init_cnpg_gke.sh      — CNPG setup for GKE with Workload Identity
  init_knoe_auth.sh     — knoe-auth OIDC service init
  init_knoe_users.sh    — user provisioning
  init_redis.sh         — Redis init
  init_oauth2_proxy.sh / init_oauth2_proxy_prole.sh — OAuth2 proxy setup
  init_grafana_oauth.sh / init_grafana_oauth_prole.sh — Grafana OAuth wiring
  init_1password.sh     — 1Password Connect init
  init_min.sh           — minimal bootstrap

Co-authored-by: Junie <junie@jetbrains.com>
2026-05-23 21:31:27 -07:00

1552 lines
47 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
set -euo pipefail
# init_monitoring.sh
# Purpose:
# - Configure k3d environment for monitoring (Prometheus and Grafana)
# - Setup kube-prometheus-stack and CNPG prometheus rules
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
PROLE_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
# Load environment and config via knoe_cfg.sh
# shellcheck disable=SC1090
source "$SCRIPT_DIR/knoe_cfg.sh"
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
knoe_set_mode "${2:-}"
shift 2
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
knoe_set_mode "${1#*=}"
shift
fi
if [[ -z "${KNOE_SERVICE:-}" ]]; then
echo "ERROR: KNOE_SERVICE is not defined. Provide KNOE_HOME/env.sh or ~/.knoe/env.sh" >&2
exit 1
fi
GRAFANA_RELEASE="grafana"
LEGACY_GRAFANA_RELEASE="grafana-knoe"
ts() {
date "+%Y-%m-%d %H:%M:%S"
}
log() {
echo "==> [$(ts)] $*"
}
err() {
echo "ERROR: [$(ts)] $*" >&2
}
section() {
echo "==> [$(ts)] --- $* ---"
}
ensure_tools() {
for t in helm kubectl curl jq; do
command -v "$t" >/dev/null || { err "Missing required tool: $t"; exit 1; }
done
}
kubectl_retry() {
local attempts="${KUBECTL_RETRY_ATTEMPTS:-5}"
local delay="${KUBECTL_RETRY_DELAY:-2}"
local i
for (( i=1; i<=attempts; i++ )); do
if kubectl "$@"; then
return 0
fi
if (( i >= attempts )); then
return 1
fi
sleep "$delay"
delay=$(( delay * 2 ))
done
}
ensure_namespace() {
local ns="$1"
if ! kubectl get namespace "$ns" >/dev/null 2>&1; then
log "Creating namespace '$ns' ..."
kubectl create namespace "$ns" >/dev/null 2>&1 || true
fi
}
storage_class_exists() {
local sc="$1"
[[ -n "$sc" ]] || return 1
kubectl get storageclass "$sc" >/dev/null 2>&1
}
default_storage_class() {
kubectl get storageclass -o jsonpath='{range .items[?(@.metadata.annotations.storageclass\.kubernetes\.io/is-default-class=="true")]}{.metadata.name}{"\n"}{end}' 2>/dev/null | head -n1
}
monitoring_mode() {
local mode
mode="${KNOE_MODE:-${DEPLOYMENT_MODE:-}}"
mode=$(printf '%s' "$mode" | tr '[:upper:]' '[:lower:]')
case "$mode" in
gke|prod|k8s)
echo "k8s"
;;
k3s|k3d)
echo "$mode"
;;
*)
echo "$mode"
;;
esac
}
choose_monitoring_storage_class() {
local mode
mode=$(monitoring_mode)
if [[ -n "${MONITORING_STORAGE_CLASS:-}" ]]; then
echo "$MONITORING_STORAGE_CLASS"
return 0
fi
if [[ "$mode" == "k3s" ]]; then
if storage_class_exists "merlin-local-iscsi"; then
echo "merlin-local-iscsi"
return 0
fi
# Backward compatibility (older runs created knoe-monitoring-<volumeId>)
if storage_class_exists "knoe-monitoring-d004"; then
echo "knoe-monitoring-d004"
return 0
fi
fi
local default_sc
default_sc=$(default_storage_class)
if [[ -n "$default_sc" ]]; then
echo "$default_sc"
return 0
fi
if [[ "$mode" == "k3s" || "$mode" == "k3d" ]] && storage_class_exists "local-path"; then
echo "local-path"
return 0
fi
echo ""
}
monitoring_storage_class_for_role() {
local role="$1"
local base="${MONITORING_STORAGE_CLASS_SELECTED:-}"
[[ -n "${base:-}" ]] || return 0
# For the static local-PV setup on the primary k3s node, use dedicated
# storageClasses per component to avoid nondeterministic PV binding.
if [[ "$(monitoring_mode)" == "k3s" && "$base" == "merlin-local-iscsi" ]]; then
local candidate
candidate="${base}-${role}"
if storage_class_exists "$candidate"; then
echo "$candidate"
else
echo "$base"
fi
return 0
fi
echo "$base"
}
validate_monitoring_storage_classes() {
local selected="${MONITORING_STORAGE_CLASS_SELECTED:-}"
[[ -n "${selected:-}" ]] || return 0
local role sc
local -a missing=()
local -a seen=()
for role in prometheus alertmanager grafana; do
sc=$(monitoring_storage_class_for_role "$role")
[[ -n "${sc:-}" ]] || continue
local already_seen=0
local seen_sc
for seen_sc in "${seen[@]}"; do
if [[ "$seen_sc" == "$sc" ]]; then
already_seen=1
break
fi
done
(( already_seen == 1 )) && continue
seen+=("$sc")
if ! storage_class_exists "$sc"; then
missing+=("$sc")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
local mode
mode=$(monitoring_mode)
err "Monitoring storageClass validation failed for mode '${mode:-unknown}': rendered class(es) not found: ${missing[*]}. Configure MONITORING_STORAGE_CLASS in conf/gke.cfg (or the selected cluster config) to an existing StorageClass."
return 1
fi
return 0
}
monitoring_nodes_available() {
kubectl get nodes -l "prole.org/node-role=general" -o name 2>/dev/null | grep -q .
}
resolve_monitoring_primary_node() {
local node
if [[ -n "${MONITORING_PRIMARY_NODE:-}" ]]; then
printf '%s' "${MONITORING_PRIMARY_NODE}"
return 0
fi
if kubectl get node merlin.prole.org >/dev/null 2>&1; then
printf '%s' "merlin.prole.org"
return 0
fi
node=$(kubectl get nodes -l "prole.org/node-role=general" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -n "${node:-}" ]]; then
printf '%s' "$node"
return 0
fi
node=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
printf '%s' "$node"
}
prepare_k3s_monitoring_data_dirs() {
local node="$1"
# NOTE: Historically this came from knoe.cfg. For now we pin it to the
# monitoring deployment default and allow overrides via env var.
local base="${PROLE_MONITORING_DATA_DIR:-/synology/d004}"
base="${base%/}"
if [[ -z "${node:-}" ]]; then
err "Could not resolve a monitoring node to prepare storage on"
return 1
fi
local parent base_name
parent=$(dirname "$base")
base_name=$(basename "$base")
section "Preparing monitoring data directories on node '${node}'"
log "Checking base directory exists on node: ${base}"
local job_ns job_name timeout image
job_ns="${MONITORING_DIRPREP_NAMESPACE:-kube-system}"
job_name="${MONITORING_DIRPREP_JOB_NAME:-knoe-monitoring-dirprep}"
timeout="${MONITORING_DIRPREP_TIMEOUT:-600s}"
image="${MONITORING_DIRPREP_IMAGE:-alpine:3.20}"
kubectl -n "$job_ns" delete job "$job_name" --ignore-not-found --wait=true --timeout=120s >/dev/null 2>&1 || true
cat <<EOF | kubectl apply -f - >/dev/null
apiVersion: batch/v1
kind: Job
metadata:
name: ${job_name}
namespace: ${job_ns}
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
nodeName: ${node}
containers:
- name: dirprep
image: ${image}
securityContext:
runAsUser: 0
runAsGroup: 0
command: ["/bin/sh", "-ec"]
args:
- |
BASE_DIR="/host-parent/${base_name}"
echo "[dirprep] node=${node} base=${base}"
if [ ! -d "\${BASE_DIR}" ]; then
echo "[dirprep][ERROR] Base monitoring directory does not exist on node: ${base}" >&2
echo "[dirprep][ERROR] Ensure the mount exists and is mounted on node '${node}'." >&2
exit 2
fi
for d in prometheus alertmanager grafana; do
mkdir -p "\${BASE_DIR}/\${d}"
chmod 0777 "\${BASE_DIR}/\${d}" 2>/dev/null || true
done
echo "[dirprep][OK] Prepared ${base}/{prometheus,alertmanager,grafana}"
volumeMounts:
- name: host-parent
mountPath: /host-parent
volumes:
- name: host-parent
hostPath:
path: ${parent}
type: Directory
EOF
if ! kubectl -n "$job_ns" wait --for=condition=complete "job/${job_name}" --timeout="$timeout" >/dev/null 2>&1; then
err "Monitoring directory preparation job did not complete successfully (job=${job_name} ns=${job_ns})."
set +e
kubectl -n "$job_ns" logs "job/${job_name}" 2>/dev/null || true
kubectl -n "$job_ns" describe "job/${job_name}" 2>/dev/null || true
kubectl -n "$job_ns" get pod -l job-name="${job_name}" -o wide 2>/dev/null || true
set -e
return 1
fi
set +e
kubectl -n "$job_ns" logs "job/${job_name}" 2>/dev/null || true
kubectl -n "$job_ns" delete job "$job_name" --ignore-not-found >/dev/null 2>&1 || true
set -e
log "[OK] Monitoring storage paths prepared under ${base}"
}
apply_k3s_monitoring_local_pvs() {
local mode="$1"
[[ "$mode" == "k3s" ]] || return 0
local node
node=$(resolve_monitoring_primary_node)
if [[ -z "${node:-}" ]]; then
err "No Kubernetes node found; cannot prepare k3s monitoring local PV directories"
return 1
fi
local base volume_id sc_base sc_prom sc_am sc_graf
base="${PROLE_MONITORING_DATA_DIR:-/synology/d004}"
base="${base%/}"
volume_id=$(basename "$base")
if [[ -z "${volume_id:-}" || "$volume_id" == "/" || "$volume_id" == "." ]]; then
err "Invalid PROLE_MONITORING_DATA_DIR: '$base'"
return 1
fi
sc_base="merlin-local-iscsi"
sc_prom="${sc_base}-prometheus"
sc_am="${sc_base}-alertmanager"
sc_graf="${sc_base}-grafana"
prepare_k3s_monitoring_data_dirs "$node"
section "Applying monitoring StorageClass and PersistentVolumes"
log "Applying monitoring StorageClasses ('${sc_base}', '${sc_prom}', '${sc_am}', '${sc_graf}') ..."
cat <<EOF | kubectl apply -f -
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ${sc_base}
provisioner: kubernetes.io/no-provisioner
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ${sc_prom}
provisioner: kubernetes.io/no-provisioner
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ${sc_am}
provisioner: kubernetes.io/no-provisioner
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ${sc_graf}
provisioner: kubernetes.io/no-provisioner
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
EOF
log "Applying monitoring PersistentVolumes (local paths under ${base}) ..."
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolume
metadata:
name: merlin-local-iscsi-${volume_id}-prometheus
labels:
synology.storage/role: prometheus
synology.storage/volume: ${volume_id}
spec:
capacity:
storage: 30Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
storageClassName: ${sc_prom}
persistentVolumeReclaimPolicy: Retain
local:
path: ${base}/prometheus
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- ${node}
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: merlin-local-iscsi-${volume_id}-alertmanager
labels:
synology.storage/role: alertmanager
synology.storage/volume: ${volume_id}
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
storageClassName: ${sc_am}
persistentVolumeReclaimPolicy: Retain
local:
path: ${base}/alertmanager
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- ${node}
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: merlin-local-iscsi-${volume_id}-grafana
labels:
synology.storage/role: grafana
synology.storage/volume: ${volume_id}
spec:
capacity:
storage: 10Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
storageClassName: ${sc_graf}
persistentVolumeReclaimPolicy: Retain
local:
path: ${base}/grafana
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- ${node}
EOF
# Default to the local-PV storageClass on k3s unless explicitly overridden.
if [[ -z "${MONITORING_STORAGE_CLASS:-}" ]]; then
MONITORING_STORAGE_CLASS="${sc_base}"
export MONITORING_STORAGE_CLASS
fi
}
render_node_selector() {
local indent="$1"
cat <<EOF
${indent}nodeSelector:
${indent} prole.org/node-role: general
EOF
}
render_primary_node_selector() {
local indent="$1"
local mode
mode="${KNOE_MODE:-${DEPLOYMENT_MODE:-}}"
[[ "${mode}" == "k3s" ]] || { render_node_selector "$indent"; return 0; }
local node
node=$(resolve_monitoring_primary_node)
if [[ -n "${node:-}" ]]; then
cat <<EOF
${indent}nodeSelector:
${indent} kubernetes.io/hostname: ${node}
EOF
else
render_node_selector "$indent"
fi
}
render_tolerations() {
local indent="$1"
# On k3s, we pin monitoring workloads to the primary node for local-PV storage.
# That node is typically tainted to keep non-monitoring workloads off it.
local mode
mode="${KNOE_MODE:-${DEPLOYMENT_MODE:-}}"
local key value effect
key="${MONITORING_TAINT_KEY:-prole.org/monitoring}"
value="${MONITORING_TAINT_VALUE:-true}"
effect="${MONITORING_TAINT_EFFECT:-NoSchedule}"
if [[ "$mode" == "k3s" ]]; then
cat <<EOF
${indent}tolerations:
${indent} - key: "${key}"
${indent} operator: "Equal"
${indent} value: "${value}"
${indent} effect: "${effect}"
EOF
fi
}
render_node_affinity() {
local indent="$1"
cat <<EOF
${indent}affinity:
${indent} nodeAffinity:
${indent} requiredDuringSchedulingIgnoredDuringExecution:
${indent} nodeSelectorTerms:
${indent} - matchExpressions:
${indent} - key: prole.org/node-role
${indent} operator: In
${indent} values:
${indent} - general
EOF
}
render_primary_node_affinity() {
local indent="$1"
local mode
mode="${KNOE_MODE:-${DEPLOYMENT_MODE:-}}"
[[ "${mode}" == "k3s" ]] || { render_node_affinity "$indent"; return 0; }
local node
node=$(resolve_monitoring_primary_node)
if [[ -n "${node:-}" ]]; then
cat <<EOF
${indent}affinity:
${indent} nodeAffinity:
${indent} requiredDuringSchedulingIgnoredDuringExecution:
${indent} nodeSelectorTerms:
${indent} - matchExpressions:
${indent} - key: kubernetes.io/hostname
${indent} operator: In
${indent} values:
${indent} - ${node}
EOF
else
render_node_affinity "$indent"
fi
}
render_prometheus_storage() {
local indent="$1"
local size="$2"
if [[ -n "${MONITORING_STORAGE_CLASS_SELECTED:-}" ]]; then
local sc
sc=$(monitoring_storage_class_for_role "prometheus")
cat <<EOF
${indent}storageSpec:
${indent} volumeClaimTemplate:
${indent} spec:
${indent} storageClassName: ${sc}
${indent} accessModes: ["ReadWriteOnce"]
${indent} resources:
${indent} requests:
${indent} storage: ${size}
EOF
fi
}
render_alertmanager_storage() {
local indent="$1"
local size="$2"
if [[ -n "${MONITORING_STORAGE_CLASS_SELECTED:-}" ]]; then
local sc
sc=$(monitoring_storage_class_for_role "alertmanager")
cat <<EOF
${indent}storage:
${indent} volumeClaimTemplate:
${indent} spec:
${indent} storageClassName: ${sc}
${indent} accessModes: ["ReadWriteOnce"]
${indent} resources:
${indent} requests:
${indent} storage: ${size}
EOF
fi
}
render_grafana_persistence() {
local indent="$1"
local size="$2"
if [[ -n "${MONITORING_STORAGE_CLASS_SELECTED:-}" ]]; then
local sc
sc=$(monitoring_storage_class_for_role "grafana")
cat <<EOF
${indent}persistence:
${indent} enabled: true
${indent} type: sts
${indent} storageClassName: ${sc}
${indent} accessModes: ["ReadWriteOnce"]
${indent} size: ${size}
EOF
else
cat <<EOF
${indent}persistence:
${indent} enabled: false
EOF
fi
}
render_grafana_external_url() {
local indent="$1"
local host="${SERVICE_HOSTNAME:-}"
host="${host//$'\n'/}"
host="${host//$'\r'/}"
if [[ -z "$host" ]]; then
return 0
fi
local sso_enabled="${PROLE_GRAFANA_SSO_ENABLED:-0}"
local sso_block=""
case "$sso_enabled" in
1|true|TRUE|True|yes|YES|on|ON)
sso_block=$(cat <<EOF
${indent} auth.proxy:
${indent} enabled: true
${indent} header_name: X-WEBAUTH-USER
${indent} header_property: username
${indent} auto_sign_up: true
${indent} auth:
${indent} disable_login_form: true
${indent} auth.anonymous:
${indent} enabled: false
EOF
)
;;
esac
local google_client_id="${GRAFANA_GOOGLE_CLIENT_ID:-}"
local google_client_secret="${GRAFANA_GOOGLE_CLIENT_SECRET:-}"
local google_block=""
if [[ -n "$google_client_id" && -n "$google_client_secret" ]]; then
google_block=$(cat <<EOF
${indent} auth.google:
${indent} enabled: true
${indent} client_id: "${google_client_id}"
${indent} client_secret: "${google_client_secret}"
${indent} scopes: openid email profile
${indent} auth_url: https://accounts.google.com/o/oauth2/v2/auth
${indent} token_url: https://accounts.google.com/o/oauth2/token
${indent} api_url: https://www.googleapis.com/oauth2/v3/userinfo
${indent} allowed_domains: knoey.com
${indent} use_pkce: true
EOF
)
fi
cat <<EOF
${indent}grafana.ini:
${indent} server:
${indent} domain: "${host}"
${indent} root_url: "https://${host}/"
${indent} serve_from_sub_path: false
${sso_block}${google_block}
EOF
}
cleanup_pending_pvcs() {
local ns="$1"
local expected_sc="$2"
local pending
pending=$(kubectl -n "$ns" get pvc -o json | jq -r '.items[] | select(.status.phase=="Pending") | "\(.metadata.name)|\(.spec.storageClassName // "")"' || true)
while IFS='|' read -r pvc_name pvc_sc; do
[[ -n "$pvc_name" ]] || continue
if [[ -n "$pvc_sc" ]] && ! storage_class_exists "$pvc_sc"; then
log "Deleting Pending PVC '$pvc_name' with missing storageClass '$pvc_sc' ..."
kubectl -n "$ns" delete pvc "$pvc_name" >/dev/null 2>&1 || true
continue
fi
if [[ -n "$expected_sc" && -n "$pvc_sc" && "$pvc_sc" != "$expected_sc" ]]; then
log "Deleting Pending PVC '$pvc_name' with storageClass '$pvc_sc' (expected '$expected_sc') ..."
kubectl -n "$ns" delete pvc "$pvc_name" >/dev/null 2>&1 || true
fi
done <<< "$pending"
}
apply_grafana_dashboard() {
local ns="$1"
local prom_uid="${GRAFANA_PROMETHEUS_DATASOURCE_UID:-prometheus}"
local dashboard_path=""
if [[ -n "${PROLE_GRAFANA_DASHBOARD_PATH:-}" ]]; then
dashboard_path="$PROLE_GRAFANA_DASHBOARD_PATH"
elif [[ -n "${KNOE_SERVICE:-}" && -f "$KNOE_SERVICE/knoe-db/grafana-dashboard.json" ]]; then
dashboard_path="$KNOE_SERVICE/knoe-db/grafana-dashboard.json"
elif [[ -f "$PROLE_ROOT/knoe-db/grafana-dashboard.json" ]]; then
dashboard_path="$PROLE_ROOT/knoe-db/grafana-dashboard.json"
fi
if [[ -z "$dashboard_path" || ! -f "$dashboard_path" ]]; then
log "Grafana dashboard not found; skipping."
return 0
fi
local tmp
tmp=$(mktemp)
# Pass the dashboard JSON through unchanged. The source JSON templates the
# datasource UID via the dashboard's own `DS_PROMETHEUS` variable (resolved
# at render time by Grafana, configurable per-user via the Datasource
# dropdown). The earlier sed-substitution to a fixed `prom_uid` baked
# `uid: prometheus` into every variable + panel definition, defeating that
# template — the dashboard then queried only the default Prometheus
# datasource regardless of what the user selected. With cross-cluster
# CNPG metrics on `cnpg-prometheus` (commit 09f2c1a), passing through
# unchanged is required for variable dropdowns + panels to follow the
# Datasource selector.
cp "$dashboard_path" "$tmp"
: "$prom_uid" # silence unused-var warning
local cm_yaml
cm_yaml=$(mktemp)
kubectl_retry -n "$ns" delete configmap knoe-db-grafana-dashboard --ignore-not-found >/dev/null 2>&1 || true
kubectl -n "$ns" create configmap knoe-db-grafana-dashboard --from-file=knoe-db.json="$tmp" --dry-run=client -o yaml > "$cm_yaml"
kubectl_retry -n "$ns" apply --server-side --field-manager=knoe-init-monitoring --force-conflicts -f "$cm_yaml" >/dev/null
kubectl_retry -n "$ns" label configmap knoe-db-grafana-dashboard grafana_dashboard=1 --overwrite >/dev/null
rm -f "$tmp" "$cm_yaml"
}
apply_grafana_datasource() {
# No-op: kube-prometheus-stack already provisions the Prometheus + Alertmanager
# datasources via its own ConfigMap (`<release>-grafana-datasource`). The
# earlier extra `knoe-grafana-datasource` ConfigMap created here was a
# duplicate of that — it registered the same `uid: prometheus`, which collided
# with the chart's datasource and caused Grafana's provisioning reload to
# error out (HTTP 500 → datasources never refreshed).
#
# Additional datasources (e.g. the cross-cluster `cnpg-prometheus` pointing
# at the DB-cluster Prometheus) are wired through the chart values at
# `monitoring/kps-values-gke.yaml` `grafana.additionalDataSources`, NOT
# through this script. Keeping this function as a no-op so call sites don't
# need to change.
return 0
}
helm_release_status() {
local release="$1"
local ns="$2"
helm status "$release" -n "$ns" -o json 2>/dev/null | jq -r '.info.status' 2>/dev/null || true
}
wait_for_helm_release() {
local release="$1"
local ns="$2"
local timeout="${HELM_WAIT_TIMEOUT:-${MONITORING_PENDING_TIMEOUT:-600}}"
local interval="${HELM_WAIT_INTERVAL:-${MONITORING_PENDING_INTERVAL:-5}}"
local start
start=$(date +%s)
while true; do
local status
status=$(helm_release_status "$release" "$ns")
if [[ -z "$status" || "$status" == "null" ]]; then
return 0
fi
case "$status" in
pending-*)
if (( $(date +%s) - start > timeout )); then
err "Timed out waiting for Helm release '$release' in '$ns' (status=$status)."
return 1
fi
log "Helm release '$release' is $status; waiting... (elapsed=$(( $(date +%s) - start ))s, timeout=${timeout}s)"
sleep "$interval"
;;
*)
return 0
;;
esac
done
}
wait_for_monitoring_resources() {
local ns="$1"
local timeout="${MONITORING_READY_TIMEOUT:-600s}"
section "Waiting for Prometheus stack resources to settle (timeout=$timeout)"
set +e
kubectl -n "$ns" rollout status deployment/kps-kube-prometheus-stack-operator --timeout="$timeout" >/dev/null 2>&1
kubectl -n "$ns" rollout status deployment/kps-kube-state-metrics --timeout="$timeout" >/dev/null 2>&1
kubectl -n "$ns" rollout status statefulset/prometheus-kps-kube-prometheus-stack-prometheus --timeout="$timeout" >/dev/null 2>&1
kubectl -n "$ns" rollout status statefulset/alertmanager-kps-kube-prometheus-stack-alertmanager --timeout="$timeout" >/dev/null 2>&1
set -e
}
collect_monitoring_diagnostics() {
local ns="$1"
local release="$2"
section "Monitoring diagnostics (namespace=$ns release=$release)"
set +e
echo "[diag] helm status ${release} -n ${ns}" >&2
helm status "$release" -n "$ns" >&2
echo "[diag] helm history ${release} -n ${ns}" >&2
helm history "$release" -n "$ns" >&2
echo "[diag] kubectl -n ${ns} get all" >&2
kubectl -n "$ns" get all -o wide >&2
echo "[diag] kubectl -n ${ns} get pvc" >&2
kubectl -n "$ns" get pvc -o wide >&2
echo "[diag] kubectl -n ${ns} get events (last 100)" >&2
kubectl -n "$ns" get events --sort-by=.lastTimestamp 2>/dev/null | tail -n 100 >&2
echo "[diag] kubectl -n ${ns} get pods (not Running)" >&2
kubectl -n "$ns" get pods --field-selector=status.phase!=Running -o wide >&2
# Best-effort describe of non-Running pods (helps pinpoint scheduling/PVC/image issues)
local pod_names
pod_names=$(kubectl -n "$ns" get pods --no-headers 2>/dev/null | awk '$3 != "Running" {print $1}' || true)
local p
for p in $pod_names; do
echo "[diag] kubectl -n ${ns} describe pod/${p}" >&2
kubectl -n "$ns" describe pod "$p" >&2
done
local pending_pvcs
pending_pvcs=$(kubectl -n "$ns" get pvc --no-headers 2>/dev/null | awk '$2 == "Pending" {print $1}' || true)
local pvc
for pvc in $pending_pvcs; do
echo "[diag] kubectl -n ${ns} describe pvc/${pvc}" >&2
kubectl -n "$ns" describe pvc "$pvc" >&2
done
set -e
}
wait_for_namespace_deleted() {
local ns="$1"
local timeout="${MONITORING_RESET_TIMEOUT:-300}"
local interval="${MONITORING_RESET_INTERVAL:-2}"
local start
start=$(date +%s)
while true; do
if ! kubectl get namespace "$ns" >/dev/null 2>&1; then
return 0
fi
if (( $(date +%s) - start > timeout )); then
err "Timed out waiting for namespace '$ns' deletion (timeout=${timeout}s)."
return 1
fi
log "Waiting for namespace '$ns' to terminate... (elapsed=$(( $(date +%s) - start ))s, timeout=${timeout}s)"
sleep "$interval"
done
}
stabilize_cluster_after_reset() {
local timeout="${MONITORING_STABILIZE_TIMEOUT:-120}"
section "Waiting for cluster to stabilize (timeout=${timeout}s)"
set +e
kubectl wait --for=condition=Ready nodes --all --timeout="${timeout}s" >/dev/null 2>&1
set -e
}
reset_monitoring_namespace() {
local ns="$1"
local release="$2"
local helm_timeout="${MONITORING_HELM_TIMEOUT:-${HELM_TIMEOUT:-10m}}"
section "Resetting monitoring (namespace=$ns release=$release)"
set +e
if helm status "$release" -n "$ns" >/dev/null 2>&1; then
log "Uninstalling Helm release '$release' from namespace '$ns' (timeout=$helm_timeout)..."
helm uninstall "$release" -n "$ns" --wait --timeout "$helm_timeout" >/dev/null 2>&1 || true
fi
log "Deleting namespace '$ns' ..."
kubectl delete namespace "$ns" >/dev/null 2>&1 || true
set -e
wait_for_namespace_deleted "$ns" || true
stabilize_cluster_after_reset || true
ensure_namespace "$ns"
}
reset_monitoring_release() {
local ns="$1"
local release="$2"
local helm_timeout="${MONITORING_HELM_TIMEOUT:-${HELM_TIMEOUT:-10m}}"
section "Resetting Helm release only (namespace=$ns release=$release)"
set +e
if helm status "$release" -n "$ns" >/dev/null 2>&1; then
log "Uninstalling Helm release '$release' from namespace '$ns' (timeout=$helm_timeout)..."
helm uninstall "$release" -n "$ns" --wait --timeout "$helm_timeout" >/dev/null 2>&1 || true
fi
set -e
}
helm_upgrade_with_retry() {
local release="$1"
local ns="$2"
local chart="$3"
shift 3
local attempts="${HELM_UPGRADE_RETRIES:-5}"
local delay="${HELM_RETRY_DELAY:-5}"
local helm_timeout="${MONITORING_HELM_TIMEOUT:-${HELM_TIMEOUT:-10m}}"
local max_resets="${MONITORING_RESET_RETRIES:-1}"
local reset_scope="${MONITORING_RESET_SCOPE:-namespace}"
local reset_count=0
local attempt out rc
local delay_current
delay_current="$delay"
for ((attempt=1; attempt<=attempts; attempt++)); do
if ! wait_for_helm_release "$release" "$ns"; then
collect_monitoring_diagnostics "$ns" "$release" || true
if (( reset_count < max_resets )); then
reset_count=$(( reset_count + 1 ))
if [[ "$reset_scope" == "release" ]]; then
reset_monitoring_release "$ns" "$release" || true
else
reset_monitoring_namespace "$ns" "$release" || true
fi
continue
fi
err "Helm release '$release' remained in a pending state and reset retries are exhausted ($reset_count/$max_resets)."
return 1
fi
set +e
out=$(helm upgrade --install "$release" "$chart" --namespace "$ns" --wait --timeout "$helm_timeout" "$@" 2>&1)
rc=$?
set -e
if [[ $rc -eq 0 ]]; then
printf '%s\n' "$out"
return 0
fi
if echo "$out" | grep -q "another operation (install/upgrade/rollback) is in progress"; then
log "Helm release '$release' is busy; retrying ($attempt/$attempts)..."
wait_for_helm_release "$release" "$ns" || true
sleep "$delay_current"
if (( delay_current < 60 )); then
delay_current=$(( delay_current * 2 ))
if (( delay_current > 60 )); then
delay_current=60
fi
fi
continue
fi
# Helm/k8s timeouts: diagnose + reset + retry.
if echo "$out" | grep -Eq "timed out waiting for the condition|context deadline exceeded"; then
err "Helm upgrade timed out (timeout=$helm_timeout)."
echo "$out" >&2
collect_monitoring_diagnostics "$ns" "$release" || true
if (( reset_count < max_resets )); then
reset_count=$(( reset_count + 1 ))
if [[ "$reset_scope" == "release" ]]; then
reset_monitoring_release "$ns" "$release" || true
else
reset_monitoring_namespace "$ns" "$release" || true
fi
continue
fi
return 1
fi
# Transient Kubernetes API/transport failures (common on small/loaded k3s clusters during CRD installs)
if echo "$out" | grep -Eq "stream error: stream ID|INTERNAL_ERROR|the server was unable to return a response in the time allotted|i/o timeout|EOF"; then
log "Transient error during Helm upgrade; retrying ($attempt/$attempts)..."
echo "$out" >&2
sleep "$delay_current"
if (( delay_current < 60 )); then
delay_current=$(( delay_current * 2 ))
if (( delay_current > 60 )); then
delay_current=60
fi
fi
continue
fi
echo "$out" >&2
return "$rc"
done
err "Helm upgrade failed after $attempts attempts for release '$release' in '$ns'."
return 1
}
openbao_url() {
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
echo "$PROLE_OPENBAO_URL"
return 0
fi
if knoe_is_in_cluster; then
echo "http://openbao.${SERVICE_NAMESPACE:-${NAMESPACE:-default}}.svc.cluster.local:8200"
return 0
fi
# In k3s mode, scripts run outside the cluster must reach OpenBao via the k3s host
# (never via localhost or kubectl port-forward).
if [[ "${KNOE_MODE:-${DEPLOYMENT_MODE:-}}" == "k3s" ]]; then
if command -v _knoe_host_from_url >/dev/null 2>&1; then
local host
host=$(_knoe_host_from_url "${PROLE_K3S_SERVER:-${K3S_SERVER_URL:-}}")
if [[ -n "${host:-}" ]]; then
echo "http://${host}:8200"
return 0
fi
fi
echo ""
return 0
fi
if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:8200"
return 0
fi
echo ""
return 0
}
openbao_token() {
if [[ -f "$KNOE_SERVICE/secrets/openbao-root-token" ]]; then
cat "$KNOE_SERVICE/secrets/openbao-root-token"
else
echo "${OPENBAO_ROOT_TOKEN:-}"
fi
}
fetch_openbao_secret() {
local path="$1"
local key="$2"
local token url
token=$(openbao_token)
url=$(openbao_url)
if [[ -z "$token" || -z "$url" ]]; then
echo ""
return 0
fi
curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/$path" | jq -r ".data.data.\"$key\"" || echo ""
}
resolve_grafana_password() {
if [[ -z "${GRAFANA_ADMIN_PASSWORD:-}" || "${GRAFANA_ADMIN_PASSWORD}" == '${OPENBAO:'* || "${GRAFANA_ADMIN_PASSWORD}" == '${KNOE_SECRET:'* ]]; then
local fetched
fetched=$(fetch_openbao_secret "knoe/${NAMESPACE:-default}/monitoring" "grafana_admin_password")
if [[ -n "$fetched" && "$fetched" != "null" ]]; then
GRAFANA_ADMIN_PASSWORD="$fetched"
fi
fi
if [[ -z "${GRAFANA_ADMIN_PASSWORD:-}" || "${GRAFANA_ADMIN_PASSWORD}" == '${OPENBAO:'* || "${GRAFANA_ADMIN_PASSWORD}" == '${KNOE_SECRET:'* ]]; then
local db_pw="${DB_PASSWORD:-}"
if [[ -z "$db_pw" || "$db_pw" == '${OPENBAO:'* || "$db_pw" == '${KNOE_SECRET:'* ]]; then
local fetched_db
fetched_db=$(fetch_openbao_secret "knoe/${NAMESPACE:-default}/db" "password")
if [[ -n "$fetched_db" && "$fetched_db" != "null" ]]; then
db_pw="$fetched_db"
fi
fi
if [[ -n "$db_pw" ]]; then
GRAFANA_ADMIN_PASSWORD="$db_pw"
fi
fi
# Strip any trailing newlines/carriage-returns that may have crept in via
# knoe.cfg parsing, file reads, or shell substitution edge-cases.
local _pw
_pw="${GRAFANA_ADMIN_PASSWORD:-}"
_pw="${_pw//$'\n'/}"
_pw="${_pw//$'\r'/}"
GRAFANA_ADMIN_PASSWORD="$_pw"
}
write_grafana_password_to_openbao() {
local token url
token=$(openbao_token)
url=$(openbao_url)
if [[ -z "$token" || -z "$url" || -z "${GRAFANA_ADMIN_PASSWORD:-}" ]]; then
return 0
fi
local _clean_grafana_pw
_clean_grafana_pw="${GRAFANA_ADMIN_PASSWORD//$'\n'/}"
_clean_grafana_pw="${_clean_grafana_pw//$'\r'/}"
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$url/v1/kv/data/knoe/${NAMESPACE:-default}/monitoring" \
-d "{\"data\":{\"grafana_admin_password\":\"$_clean_grafana_pw\"}}" >/dev/null || true
}
cleanup_grafana_rbac_conflicts() {
local release="$GRAFANA_RELEASE"
local ns="$NAMESPACE"
local cr="${release}-clusterrole"
local crb="${release}-clusterrolebinding"
local rel_ns rel_name
if kubectl get clusterrole "$cr" >/dev/null 2>&1; then
rel_ns=$(kubectl get clusterrole "$cr" -o jsonpath='{.metadata.annotations.meta\.helm\.sh/release-namespace}' 2>/dev/null || true)
rel_name=$(kubectl get clusterrole "$cr" -o jsonpath='{.metadata.annotations.meta\.helm\.sh/release-name}' 2>/dev/null || true)
if [[ -n "$rel_ns" && "$rel_ns" != "$ns" ]]; then
log "Detected existing ClusterRole '$cr' owned by release '${rel_name:-unknown}' in namespace '$rel_ns'."
if [[ -n "$rel_name" ]] && helm status "$rel_name" -n "$rel_ns" >/dev/null 2>&1; then
log "Uninstalling old Grafana release '$rel_name' from '$rel_ns' ..."
helm uninstall "$rel_name" -n "$rel_ns" || true
fi
if kubectl get clusterrole "$cr" >/dev/null 2>&1; then
log "Deleting orphaned ClusterRole '$cr' ..."
kubectl delete clusterrole "$cr" || true
fi
fi
fi
if kubectl get clusterrolebinding "$crb" >/dev/null 2>&1; then
rel_ns=$(kubectl get clusterrolebinding "$crb" -o jsonpath='{.metadata.annotations.meta\.helm\.sh/release-namespace}' 2>/dev/null || true)
rel_name=$(kubectl get clusterrolebinding "$crb" -o jsonpath='{.metadata.annotations.meta\.helm\.sh/release-name}' 2>/dev/null || true)
if [[ -n "$rel_ns" && "$rel_ns" != "$ns" ]]; then
log "Detected existing ClusterRoleBinding '$crb' owned by release '${rel_name:-unknown}' in namespace '$rel_ns'."
if [[ -n "$rel_name" ]] && helm status "$rel_name" -n "$rel_ns" >/dev/null 2>&1; then
log "Uninstalling old Grafana release '$rel_name' from '$rel_ns' ..."
helm uninstall "$rel_name" -n "$rel_ns" || true
fi
if kubectl get clusterrolebinding "$crb" >/dev/null 2>&1; then
log "Deleting orphaned ClusterRoleBinding '$crb' ..."
kubectl delete clusterrolebinding "$crb" || true
fi
fi
fi
}
cleanup_legacy_grafana_release() {
local ns="$NAMESPACE"
if helm status "$LEGACY_GRAFANA_RELEASE" -n "$ns" >/dev/null 2>&1; then
log "Uninstalling legacy Grafana release '$LEGACY_GRAFANA_RELEASE' from '$ns' ..."
helm uninstall "$LEGACY_GRAFANA_RELEASE" -n "$ns" || true
fi
}
install_monitoring() {
ensure_tools
local monitoring_ns="monitoring"
ensure_namespace "$monitoring_ns"
section "Pre-check monitoring state"
log "Helm release status: kps=$(helm_release_status kps "$monitoring_ns"), grafana=$(helm_release_status "$GRAFANA_RELEASE" "$monitoring_ns")"
local mode
mode="${KNOE_MODE:-${DEPLOYMENT_MODE:-}}"
# For k3s service clusters using local PVs, ensure the backing host directories
# exist before applying StorageClass/PV manifests.
apply_k3s_monitoring_local_pvs "$mode"
if ! monitoring_nodes_available; then
err "No nodes labeled knoe.org/node-role=general; monitoring requires general nodes."
return 2
fi
MONITORING_STORAGE_CLASS_SELECTED=$(choose_monitoring_storage_class)
if [[ -n "$MONITORING_STORAGE_CLASS_SELECTED" ]]; then
log "Using storageClass '$MONITORING_STORAGE_CLASS_SELECTED' for monitoring PVCs."
else
log "No storageClass detected; disabling persistence for Grafana and skipping Prometheus/Alertmanager storage."
fi
if ! validate_monitoring_storage_classes; then
return 1
fi
# Dont enforce a single storageClass here: in k3s static local-PV mode we use
# per-component storageClasses (e.g. merlin-local-iscsi-prometheus|grafana|alertmanager).
cleanup_pending_pvcs "$monitoring_ns" ""
local separate_grafana
separate_grafana="${MONITORING_SEPARATE_GRAFANA:-false}"
case "$separate_grafana" in
1|true|TRUE|yes|YES|on|ON)
separate_grafana="true"
;;
*)
separate_grafana="false"
;;
esac
# Derive PV names from PROLE_MONITORING_DATA_DIR (same logic as apply_monitoring_pvs)
local _mpv_base _mpv_vid
_mpv_base="${PROLE_MONITORING_DATA_DIR:-/synology/d004}"
_mpv_base="${_mpv_base%/}"
_mpv_vid=$(basename "$_mpv_base")
pv_prom="merlin-local-iscsi-${_mpv_vid}-prometheus"
pv_am="merlin-local-iscsi-${_mpv_vid}-alertmanager"
pv_graf="merlin-local-iscsi-${_mpv_vid}-grafana"
# Release any Released monitoring PVs so new PVCs can bind (idempotent)
for _mpv in "${pv_prom}" "${pv_am}" "${pv_graf}"; do
_mpv_phase=$(kubectl get pv "$_mpv" -o jsonpath='{.status.phase}' 2>/dev/null || true)
if [[ "$_mpv_phase" == "Released" ]]; then
log "Clearing stale claimRef on Released PV '$_mpv' ..."
kubectl patch pv "$_mpv" -p '{"spec":{"claimRef":null}}' 2>/dev/null || true
fi
done
section "Installing Prometheus stack"
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts || true
helm repo update prometheus-community || true
resolve_grafana_password
if [[ -z "${GRAFANA_ADMIN_PASSWORD:-}" ]]; then
GRAFANA_ADMIN_PASSWORD="admin" # Fallback
fi
local values_file
values_file=$(mktemp)
cat > "$values_file" <<EOF
prometheus:
prometheusSpec:
$(render_primary_node_selector " ")
$(render_primary_node_affinity " ")
$(render_tolerations " ")
$(render_prometheus_storage " " "30Gi")
additionalScrapeConfigs:
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- action: keep
source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
target_label: __address__
regex: (.*?):\d+;(\d+)
replacement: \$1:\$2
- job_name: 'cnpg-metrics'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- action: keep
source_labels: [__meta_kubernetes_pod_label_cnpg_io_cluster]
regex: .+
- action: keep
source_labels: [__meta_kubernetes_pod_phase]
regex: Running
- action: replace
source_labels: [__meta_kubernetes_pod_ip]
target_label: __address__
replacement: \$1:9187
- action: replace
source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- action: replace
source_labels: [__meta_kubernetes_pod_name]
target_label: pod
- action: replace
source_labels: [__meta_kubernetes_pod_label_cnpg_io_cluster]
target_label: cluster
grafana:
enabled: $([[ "$separate_grafana" == "true" ]] && echo "false" || echo "true")
adminPassword: "${GRAFANA_ADMIN_PASSWORD}"
initChownData:
enabled: false
$(render_grafana_external_url " ")
service:
port: 80
targetPort: 3000
sidecar:
dashboards:
enabled: true
label: grafana_dashboard
labelValue: "1"
datasources:
enabled: true
label: grafana_datasource
labelValue: "1"
$(render_primary_node_selector " ")
$(render_primary_node_affinity " ")
$(render_tolerations " ")
$(render_grafana_persistence " " "10Gi")
alertmanager:
alertmanagerSpec:
$(render_primary_node_selector " ")
$(render_primary_node_affinity " ")
$(render_tolerations " ")
$(render_alertmanager_storage " " "5Gi")
prometheusOperator:
$(render_node_selector " ")
$(render_node_affinity " ")
$(render_tolerations " ")
kube-state-metrics:
$(render_node_selector " ")
$(render_node_affinity " ")
$(render_tolerations " ")
prometheus-node-exporter:
$(render_node_selector " ")
$(render_node_affinity " ")
$(render_tolerations " ")
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: NotIn
values:
- pi.prole.org
EOF
local monitoring_helm_server_side
monitoring_helm_server_side="${MONITORING_HELM_SERVER_SIDE:-}"
if [[ -z "$monitoring_helm_server_side" ]]; then
if [[ "$mode" == "k3s" ]]; then
monitoring_helm_server_side="false"
else
monitoring_helm_server_side="true"
fi
fi
local helm_apply_flags=()
case "$monitoring_helm_server_side" in
1|true|TRUE|yes|YES|on|ON)
helm_apply_flags=(--force-conflicts --server-side=true)
;;
esac
helm_upgrade_with_retry \
"kps" \
"$monitoring_ns" \
"prometheus-community/kube-prometheus-stack" \
"${helm_apply_flags[@]}" \
-f "$values_file"
rm -f "$values_file"
wait_for_monitoring_resources "$monitoring_ns" || true
section "Applying Prometheus-related additional resources"
log "Applying myrddin-node-exporter resources in namespace '$monitoring_ns'..."
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: myrddin-node-exporter
namespace: $monitoring_ns
labels:
app: myrddin-node-exporter
spec:
ports:
- name: metrics
port: 9100
targetPort: 9100
---
apiVersion: v1
kind: Endpoints
metadata:
name: myrddin-node-exporter
namespace: $monitoring_ns
subsets:
- addresses:
- ip: 10.0.0.203 # myrddin LAN IP
ports:
- name: metrics
port: 9100
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: myrddin-node-exporter
namespace: $monitoring_ns
spec:
selector:
matchLabels:
app: myrddin-node-exporter
endpoints:
- port: metrics
interval: 15s
EOF
log "Applying CNPG prometheus rules in namespace '$monitoring_ns'..."
kubectl apply --namespace "$monitoring_ns" -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/main/docs/src/samples/monitoring/prometheusrule.yaml
if [[ "$separate_grafana" == "true" ]]; then
section "Installing Grafana (after Prometheus stack is ready)"
cleanup_legacy_grafana_release
cleanup_grafana_rbac_conflicts
helm repo add grafana https://grafana.github.io/helm-charts || true
helm repo update grafana || true
resolve_grafana_password
if [[ -z "${GRAFANA_ADMIN_PASSWORD:-}" ]]; then
GRAFANA_ADMIN_PASSWORD="admin" # Fallback
fi
# Belt-and-suspenders: ensure no newline survives into the Helm values YAML.
GRAFANA_ADMIN_PASSWORD="${GRAFANA_ADMIN_PASSWORD//$'\n'/}"
GRAFANA_ADMIN_PASSWORD="${GRAFANA_ADMIN_PASSWORD//$'\r'/}"
local grafana_values
grafana_values=$(mktemp)
cat > "$grafana_values" <<EOF
fullnameOverride: kps-grafana
adminPassword: "${GRAFANA_ADMIN_PASSWORD}"
$(render_grafana_external_url "")
service:
port: 80
targetPort: 3000
sidecar:
dashboards:
enabled: true
label: grafana_dashboard
labelValue: "1"
datasources:
enabled: true
label: grafana_datasource
labelValue: "1"
$(render_primary_node_selector "")
$(render_primary_node_affinity "")
$(render_tolerations "")
$(render_grafana_persistence "" "10Gi")
EOF
MONITORING_RESET_SCOPE=release \
helm_upgrade_with_retry \
"$GRAFANA_RELEASE" \
"$monitoring_ns" \
"grafana/grafana" \
-f "$grafana_values"
rm -f "$grafana_values"
else
log "Skipping separate Grafana Helm release; Grafana is managed by kube-prometheus-stack (kps)."
fi
# Restart Grafana deployment so it picks up the (possibly updated) admin password.
# Helm upgrade updates the secret but a running pod won't re-read it without a restart.
log "Applying Grafana Prometheus datasource provisioning..."
apply_grafana_datasource "$monitoring_ns"
log "Restarting Grafana deployment to apply admin credentials..."
kubectl rollout restart deployment/kps-grafana -n "$monitoring_ns" 2>/dev/null || true
kubectl rollout status deployment/kps-grafana -n "$monitoring_ns" --timeout=60s 2>/dev/null || true
log "Applying Knoe Grafana dashboard..."
apply_grafana_dashboard "$monitoring_ns"
# Register port forwards
knoe_register_port_forward "prometheus" "$monitoring_ns" "svc/kps-kube-prometheus-stack-prometheus" "9090" "9090" "127.0.0.1" "TCP" "Prometheus"
knoe_register_port_forward "grafana" "$monitoring_ns" "svc/kps-grafana" "3000" "80" "0.0.0.0" "TCP" "Grafana"
}
check_monitoring_status() {
local monitoring_ns="monitoring"
local rc=0
# Check that the Helm releases exist and are deployed
local helm_status
helm_status=$(helm status kps -n "$monitoring_ns" -o json 2>/dev/null | jq -r '.info.status' 2>/dev/null || true)
if [[ -z "$helm_status" || "$helm_status" == "null" ]]; then
echo "[FAIL] kube-prometheus-stack Helm release not found in namespace '$monitoring_ns'"
return 1
fi
if [[ "$helm_status" != "deployed" ]]; then
echo "[FAIL] kube-prometheus-stack Helm release status: $helm_status"
rc=1
else
echo "[OK] kube-prometheus-stack Helm release is deployed"
fi
local separate_grafana
separate_grafana="${MONITORING_SEPARATE_GRAFANA:-false}"
case "$separate_grafana" in
1|true|TRUE|yes|YES|on|ON)
separate_grafana="true"
;;
*)
separate_grafana="false"
;;
esac
if [[ "$separate_grafana" == "true" ]]; then
local grafana_helm_status
grafana_helm_status=$(helm status "$GRAFANA_RELEASE" -n "$monitoring_ns" -o json 2>/dev/null | jq -r '.info.status' 2>/dev/null || true)
if [[ -z "$grafana_helm_status" || "$grafana_helm_status" == "null" ]]; then
echo "[FAIL] Grafana Helm release '$GRAFANA_RELEASE' not found in namespace '$monitoring_ns'"
rc=1
elif [[ "$grafana_helm_status" != "deployed" ]]; then
echo "[FAIL] Grafana Helm release '$GRAFANA_RELEASE' status: $grafana_helm_status"
rc=1
else
echo "[OK] Grafana Helm release '$GRAFANA_RELEASE' is deployed"
fi
else
echo "[OK] Separate Grafana Helm release is disabled (Grafana expected via kube-prometheus-stack)"
fi
# Check key deployments
local deps=("kps-kube-prometheus-stack-operator" "kps-kube-state-metrics" "kps-grafana")
for dep in "${deps[@]}"; do
local avail
avail=$(kubectl -n "$monitoring_ns" get deploy "$dep" -o jsonpath='{.status.availableReplicas}' 2>/dev/null || true)
if [[ -z "$avail" || "$avail" == "0" ]]; then
echo "[FAIL] deployment/$dep not available in namespace '$monitoring_ns'"
rc=1
else
echo "[OK] deployment/$dep available (${avail} replicas)"
fi
done
# Check Prometheus StatefulSet
local prom_ready
prom_ready=$(kubectl -n "$monitoring_ns" get statefulset prometheus-kps-kube-prometheus-stack-prometheus -o jsonpath='{.status.readyReplicas}' 2>/dev/null || true)
if [[ -z "$prom_ready" || "$prom_ready" == "0" ]]; then
echo "[FAIL] statefulset/prometheus-kps-kube-prometheus-stack-prometheus not ready"
rc=1
else
echo "[OK] Prometheus StatefulSet ready (${prom_ready} replicas)"
fi
# Check Alertmanager StatefulSet
local am_ready
am_ready=$(kubectl -n "$monitoring_ns" get statefulset alertmanager-kps-kube-prometheus-stack-alertmanager -o jsonpath='{.status.readyReplicas}' 2>/dev/null || true)
if [[ -z "$am_ready" || "$am_ready" == "0" ]]; then
echo "[FAIL] statefulset/alertmanager not ready"
rc=1
else
echo "[OK] Alertmanager StatefulSet ready (${am_ready} replicas)"
fi
return $rc
}
case "${1:-}" in
initialize)
install_monitoring
;;
status)
check_monitoring_status
;;
*)
echo "Usage: $0 {initialize|status}"
exit 1
;;
esac