Skip to content

Jarvis Helm Chart

Web frontend for Prometheus Alertmanager with persistent alert history, claims, comments, and silence management.

Database backends and multi-replica HA (leader election, snapshot distribution, failover) are covered in docs/postgres-ha.md; Kubernetes deployment guidance (incl. a CloudNativePG example) is in docs/deploy-kubernetes.md. This README covers only the chart's values and install/upgrade mechanics.

Install

bash
helm install jarvis oci://ghcr.io/kj187/charts/jarvis \
  --version <version> \
  --set clusters[0].name=production \
  --set clusters[0].alertmanagerUrl=http://alertmanager:9093

Or with a values file:

bash
helm install jarvis oci://ghcr.io/kj187/charts/jarvis --version <version> -f values.yaml

Upgrade

Follow Upgrade and rollback for version selection, database backup, the helm upgrade command, and rollback ordering.

Uninstall

bash
helm uninstall jarvis

Versioning

The chart version is decoupled from the app version. appVersion in Chart.yaml pins the default image tag; the chart version follows semver by the impact of the chart change itself — a breaking change bumps the major version, new values or resources the minor, fixes and appVersion-only bumps the patch. The chart is published automatically by .github/workflows/chart-release.yml — as part of an app release after the image is built, or on its own when a chart-only version bump lands on main. A chart is only published once the image for its appVersion exists. Published versions are immutable.

All chart changes are documented in CHANGELOG.md. Every version lists its Breaking Changes explicitly (or states that there are none) — read that section before helm upgrade across versions.

Verify the chart signature

Charts are signed keylessly. Use the maintained cosign command and provenance checks in Verify release artifacts.

Testing

The chart ships unit tests via helm-unittest. No Kubernetes cluster is needed.

bash
# Install plugin once
helm plugin install https://github.com/helm-unittest/helm-unittest --version v0.8.2

# Run all chart tests
helm unittest charts/jarvis/

# Also available via Makefile
make helm-lint    # static validation
make helm-test    # unit tests

Tests cover four suites (deployment, configmap, secret, ingress) and run automatically in the pre-commit hook (when charts/ files are staged) and in CI.

Values

KeyTypeDefaultDescription
replicaCountint1Number of replicas (for >1, use PostgreSQL backend)
image.repositorystringghcr.io/kj187/jarvisContainer image repository
image.tagstring""Image tag (defaults to chart appVersion)
image.pullPolicystringIfNotPresentImage pull policy
imagePullSecretslist[]Image pull secrets
nameOverridestring""Override chart name
fullnameOverridestring""Override fully qualified app name
serviceAccount.createbooltrueCreate a ServiceAccount
serviceAccount.annotationsobject{}ServiceAccount annotations
serviceAccount.namestring""ServiceAccount name (auto-generated when empty)
leaderElection.podLabel.enabledbooltrueLabel the current leader pod jarvis.kj187.de/role=leader (informational only — every pod serves all traffic). Renders a Role+RoleBinding (pods: get, patch) and sets automountServiceAccountToken: true on the pod; meaningful only with PostgreSQL and replicaCount/HPA > 1, harmless to leave on otherwise
podAnnotationsobject{}Pod annotations
podLabelsobject{}Extra pod labels, merged into the pod template's labels
podSecurityContextobject{runAsNonRoot: true, runAsUser: 65532, ...}Pod-level security context
securityContextobject{allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, ...}Container-level security context
service.typestringClusterIPKubernetes Service type
service.portint80Service port
metrics.serviceMonitor.enabledboolfalseCreate a Prometheus Operator ServiceMonitor for /metrics (requires the monitoring.coreos.com/v1 CRDs)
metrics.serviceMonitor.intervalstring30sScrape interval
metrics.serviceMonitor.scrapeTimeoutstring10sScrape timeout
metrics.serviceMonitor.labelsobject{}Extra labels on the ServiceMonitor (e.g. to match a kube-prometheus-stack release selector)
metrics.serviceMonitor.annotationsobject{}Extra annotations on the ServiceMonitor
metrics.serviceMonitor.relabelingslist[]Prometheus Operator Endpoint.relabelings (target relabeling before scrape)
metrics.serviceMonitor.metricRelabelingslist[]Prometheus Operator Endpoint.metricRelabelings (metric relabeling after scrape)
metrics.serviceMonitor.honorLabelsboolfalseEndpoint.honorLabels — keep false unless you specifically want Jarvis's own metric labels (e.g. cluster) to win over scrape-time labels on a collision
metrics.podAnnotationsboolfalseAdd prometheus.io/scrape, prometheus.io/port, prometheus.io/path pod annotations instead (annotation-based scraping)
ingress.enabledboolfalseEnable Ingress
ingress.classNamestring""Ingress class name
ingress.annotationsobject{}Ingress annotations
ingress.hostslist[{host: jarvis.example.com, paths: [{path: /, pathType: Prefix}]}]Ingress hosts
ingress.tlslist[]Ingress TLS configuration
config.logLevelstringinfoLog level (info or debug)
config.pollIntervalstring15sAlertmanager poll interval
config.runbookBaseURLstring""Base URL prepended to runbook label values
config.allowedOriginsstring""Comma-separated allowed CORS/WebSocket origins
config.silenceDurationsstring""Instance default durations of the Fast-Silence and Extend-silence menus, e.g. 15m,1h,4h,1d,1w,30d (m/h/d/w/y, max 12, 1m–365d). Empty = built-in defaults; users can override it in Settings
clusterslistsee belowAlertmanager cluster list (at least one required)
clusters[].namestringdefaultDisplay name for the cluster
clusters[].alertmanagerUrlstringhttp://alertmanager:9093Internal Alertmanager URL
clusters[].prometheusUrlstring""Optional Prometheus URL
clusters[].hostAliasstring""Optional browser-visible URL override
clusters[].auth.bearerTokenstring""Static bearer token sent as Authorization: Bearer <token>. Stored in a Secret.
clusters[].auth.basicAuth.usernamestring""HTTP basic auth username
clusters[].auth.basicAuth.passwordstring""HTTP basic auth password. Stored in a Secret.
clusters[].auth.oauth2.clientIdstring""OAuth2 client_credentials client ID. Enables OAuth2 for this cluster; requires tokenUrl
clusters[].auth.oauth2.clientSecretstring""OAuth2 client secret. Stored in a Secret.
clusters[].auth.oauth2.tokenUrlstring""OAuth2 token endpoint. Required when clientId is set — the render fails otherwise
clusters[].auth.oauth2.scopesstring""Comma-separated OAuth2 scopes, e.g. openid,profile
clusters[].auth.headersobject{}Arbitrary headers sent with every request, e.g. {X-Scope-OrgID: tenant1}
clusters[].auth.existingSecretstring""Existing Secret to read bearerToken/basicAuth.password/oauth2.clientSecret from instead of the values above. Missing keys are treated as unset.
clusters[].auth.existingSecretKeysobject{}Key names in existingSecret; unset entries fall back to cluster-<n>-bearer-token / cluster-<n>-basic-auth-password / cluster-<n>-oauth2-client-secret
database.dsnstring/data/jarvis.dbDatabase DSN (SQLite path or postgres:// URL; PostgreSQL recommended for production)
database.existingSecretstring""Use an existing Secret for the DSN instead
database.existingSecretKeystringdsnKey in the existing Secret
database.maxOpenConnsint10PostgreSQL connection-pool cap per pod (JARVIS_DB_MAX_OPEN_CONNS); ignored for SQLite. Keep replicaCount × maxOpenConns well below the server's max_connections
auth.providerstringnoneAuthentication mode: none, internal, or oidc
auth.modestring""Protection level when provider ≠ none: write_protect (default) or full_protect
auth.secretKeystring""JWT signing key (min 32 random bytes). Use auth.existingSecret in production.
auth.existingSecretstring""Existing K8s Secret with secret-key (and oidc-client-secret for OIDC)
auth.existingSecretKeys.secretKeystringsecret-keyKey in Secret for JARVIS_SECRET_KEY
auth.existingSecretKeys.oidcClientSecretstringoidc-client-secretKey in Secret for JARVIS_AUTH_OIDC_CLIENT_SECRET
auth.oidc.issuerstring""OIDC provider issuer URL
auth.oidc.clientIdstring""OIDC client ID
auth.oidc.clientSecretstring""OIDC client secret (stored in Secret, not ConfigMap)
auth.oidc.redirectUrlstring""OIDC redirect URL (must match provider config)
auth.oidc.scopesstringopenid,profile,emailComma-separated OIDC scopes
persistence.enabledboolfalseEnable PVC for SQLite storage (single-replica recommended)
persistence.storageClassstring""StorageClass name
persistence.accessModestringReadWriteOncePVC access mode
persistence.sizestring1GiPVC size
persistence.annotationsobject{}PVC annotations (e.g. helm.sh/resource-policy: keep)
resourcesobject{}Resource requests/limits
updateStrategy.typestring""Deployment update strategy. Empty auto-selects: Recreate when persistence.enabled — an RWO volume (EBS and friends) cannot be mounted by two pods at once, so a rolling update forces a detachment and the old pod hits disk I/O errors — and RollingUpdate otherwise. Set it explicitly to override (e.g. RollingUpdate on PostgreSQL)
autoscaling.enabledboolfalseEnable HPA (requires PostgreSQL — same reasoning as replicaCount above)
autoscaling.minReplicasint1Lower bound for the HPA
autoscaling.maxReplicasint3Upper bound for the HPA
autoscaling.targetCPUUtilizationPercentageint80Target average CPU utilization that drives scaling
podDisruptionBudget.enabledboolfalseCreate a PodDisruptionBudget — prevents voluntary disruptions (node drains, upgrades) from taking down every replica at once. Meaningful only with replicaCount/HPA > 1 (PostgreSQL)
podDisruptionBudget.minAvailableint1Minimum pods that must stay available during a voluntary disruption
nodeSelectorobject{}Node selector
tolerationslist[]Pod tolerations
affinityobject{}Pod affinity rules
topologySpreadConstraintslist[]Passed through verbatim to spec.template.spec.topologySpreadConstraints — spread replicas across nodes/zones for real HA. Meaningful only with replicaCount/HPA > 1 (PostgreSQL)
extraEnvlist[]Additional environment variables for the container
extraVolumeslist[]Additional volumes for the Pod
extraVolumeMountslist[]Additional volume mounts for the container
serviceAccountTokenProjection.enabledboolfalseMount a projected ServiceAccount token
serviceAccountTokenProjection.volumeNamestringworkload-tokenVolume and mount name
serviceAccountTokenProjection.mountPathstring/var/run/secrets/workloadMount path inside the container
serviceAccountTokenProjection.tokenFileNamestringtokenToken file name inside mountPath
serviceAccountTokenProjection.audiencestring""Token audience (e.g. vault). Empty = default Kubernetes audience
serviceAccountTokenProjection.expirationSecondsint3600Token lifetime in seconds (Kubernetes auto-rotates before expiry)

Examples

For Kubernetes production deployments, PostgreSQL is recommended. SQLite (with or without PVC) is mainly intended for single-replica or test setups.

Minimal (SQLite, ephemeral)

yaml
clusters:
  - name: production
    alertmanagerUrl: http://alertmanager.monitoring.svc:9093

SQLite with persistent storage

Recommended only for single-replica setups (replicaCount: 1).

yaml
clusters:
  - name: production
    alertmanagerUrl: http://alertmanager.monitoring.svc:9093

persistence:
  enabled: true
  size: 2Gi

PostgreSQL backend

yaml
clusters:
  - name: production
    alertmanagerUrl: http://alertmanager.monitoring.svc:9093

database:
  dsn: postgres://jarvis:secret@postgres.monitoring.svc:5432/jarvis?sslmode=require

Use an external Secret to avoid storing credentials in values:

yaml
database:
  existingSecret: jarvis-db-credentials
  existingSecretKey: dsn

Authentication

For internal accounts, OIDC, Kubernetes Secret examples, and provider-specific setup, see User authentication.

Multiple clusters

yaml
clusters:
  - name: staging
    alertmanagerUrl: http://alertmanager-staging:9093
    prometheusUrl: http://prometheus-staging:9090
  - name: production
    alertmanagerUrl: http://alertmanager-prod:9093
    prometheusUrl: http://prometheus-prod:9090
    hostAlias: https://alertmanager.prod.example.com

Alertmanager behind an authentication proxy

OAuth2, bearer-token, basic-auth, custom-header, and Secret examples are kept in Alertmanager authentication. That guide also documents the extraEnv fallback for chart versions without clusters[].auth values.

Ingress with WebSocket support

Jarvis uses WebSocket (/ws) for live alert updates. The ingress must not strip or block the Upgrade / Connection headers, and config.allowedOrigins must name the URL the browser uses — see docs/reverse-proxy.md.

ingress-nginx

ingress-nginx requires explicit WebSocket annotations:

yaml
ingress:
  enabled: true
  className: nginx
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
    nginx.ingress.kubernetes.io/configuration-snippet: |
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
  hosts:
    - host: jarvis.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: jarvis-tls
      hosts:
        - jarvis.example.com

config:
  allowedOrigins: "https://jarvis.example.com"

Vault JWT auth with projected ServiceAccount token

Use serviceAccountTokenProjection to obtain a short-lived, audience-scoped token without relying on the pod-level automountServiceAccountToken. Pair it with extraEnv to point the app at the token file.

Note: automountServiceAccountToken on the pod is otherwise driven by leaderElection.podLabel.enabled (default true — the leader pod label needs the default token to call the Kubernetes API, see the values table above). Set leaderElection.podLabel.enabled: false too if you want only the projected workload token mounted and nothing else.

yaml
serviceAccount:
  create: true
  # Annotate with the Vault role when using the Vault Agent Injector or the
  # Kubernetes auth backend directly.
  annotations:
    vault.hashicorp.com/role: jarvis

# Skip mounting the default token entirely — only the projected workload
# token below is needed.
leaderElection:
  podLabel:
    enabled: false

serviceAccountTokenProjection:
  enabled: true
  volumeName: workload-token
  mountPath: /var/run/secrets/workload
  tokenFileName: token
  audience: vault          # must match the Vault Kubernetes auth backend audience
  expirationSeconds: 3600

extraEnv:
  - name: VAULT_TOKEN_FILE
    value: /var/run/secrets/workload/token
  - name: VAULT_ADDR
    value: https://vault.example.com

The token at /var/run/secrets/workload/token is a standard Kubernetes serviceAccountToken projection — Vault's Kubernetes auth method accepts it directly via the jwt login path.

Traefik (v2 / v3)

Traefik forwards WebSocket connections out of the box — no extra annotations are required. Only add annotations if you need HTTPS redirect or custom middleware:

yaml
ingress:
  enabled: true
  className: traefik
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls: "true"
  hosts:
    - host: jarvis.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: jarvis-tls
      hosts:
        - jarvis.example.com

config:
  allowedOrigins: "https://jarvis.example.com"

Released under the Apache 2.0 License. Jarvis is not affiliated with Prometheus or Alertmanager.