Skip to content

Autopulse

Media-automation app in the media namespace (bjw-s app-template HelmRelease). Receives import/upgrade webhooks from Sonarr and Radarr and tells Jellyfin to rescan just the changed paths.

Purpose

  • Sonarr and Radarr trigger autopulse on import/upgrade; autopulse's one target is Jellyfin (http://jellyfin.media.svc.cluster.local:8096), scanned per path rather than whole-library.
  • Web UI reachable at autopulse.${SECRET_DOMAIN} via envoy-internal (internal-only); SQLite database persisted on a VolSync-backed PVC.
  • Not in scope: a Plex target, or exposing the app externally.

Design decisions

  • Jellyfin is the only target. Plex is deliberately absent. Sonarr and Radarr both already carry a native "Plex Media Server" Connect notification (updateLibrary: true, on import and upgrade), so a Plex target here would double-scan. Jellyfin has no such native notifier in either app and, with the library mounted over NFS, receives no filesystem events. That is the gap autopulse closes.
  • No rewrite: blocks. The Sonarr/Radarr root folders are /media/tv and /media/movies, and autopulse, Sonarr, Radarr and Jellyfin all mount the same export at /media, so the path a trigger sends is already the path Jellyfin expects. opts.check_path: true makes autopulse stat the file before notifying, which is why the HelmRelease mounts the media export read-only.
  • Trigger auth is the app's basic auth. autopulse has no per-trigger token; Sonarr/Radarr POST to /triggers/sonarr / /triggers/radarr with the same auth.username / auth.password as the UI.
  • SQLite, not Postgres. v2.0.0 changed the default database_url from PostgreSQL to a SQLite file (sqlite://data/autopulse.db, resolving to /app/data/autopulse.db). The minimal config embraces this: no database dependency, just a single file on a small PVC.
  • Single-file SQLite DB backed up with VolSync. The whole "database" is one file living on a PVC, so the standard VolSync component backs it up exactly like any other app's data dir, with no DB-specific dump/restore tooling needed:
  • The volsync component (added in ks.yaml spec.components, never also in app/kustomization.yaml) auto-creates the PVC autopulse (ceph-block), the NFS + remote ReplicationSources, and the restore ReplicationDestination.
  • VOLSYNC_CAPACITY: 1Gi because a SQLite DB is tiny (the component default is 5Gi, overridden down here).
  • Backup credentials reuse the shared VolSync 1Password items; no new backup secrets are introduced.
  • Writable PVC at /app/data, root FS stays read-only. The HelmRelease keeps readOnlyRootFilesystem: true; SQLite still works because the writable mount is the PVC at /app/data, not the root FS:
persistence:
  data:
    existingClaim: "{{ .Release.Name }}"
    globalMounts:
      - path: /app/data
  • Inline config.yaml in the ExternalSecret. The earlier templateFrom.configMap pointed at a ConfigMap that never existed in Git or the cluster, so it was dropped. The config is rendered inline; only the auth credentials come from 1Password, while the non-secret database_url lives in the template:
target:
  name: autopulse-secret
  template:
    engineVersion: v2
    data:
      config.yaml: |
        app:
          database_url: sqlite://data/autopulse.db
        auth:
          username: "{{ .username }}"
          password: "{{ .password }}"
dataFrom:
  - extract:
      key: autopulse

Deploy gotchas

  • An unconfigured v2 pod cannot start. With readOnlyRootFilesystem: true and no writable mount at /app/data, v2 cannot create the SQLite file. The writable data PVC (above) is mandatory, not optional.
  • existingClaim must match the VolSync PVC name. The HelmRelease uses existingClaim: "{{ .Release.Name }}", which resolves to autopulse, the exact name the VolSync component derives from APP. PVC name, claim, and ReplicationSource sourcePVC must all agree.
  • Default credentials must be changed. v2 ships admin / password defaults. Create the autopulse 1Password login item (username admin + a generated 32-char password) via op item create in the operator's signed-in shell, never the UI, never committed. The field labels username/password must match the {{ .username }} / {{ .password }} template vars. The Jellyfin API key (Dashboard → API Keys, one issued for autopulse) lives on the same item as a concealed JELLYFIN_TOKEN field.
  • The Sonarr/Radarr side is not in Git. Each app needs a Connect → Webhook (Settings → Connect) pointing at http://autopulse.media.svc.cluster.local:2875/triggers/<sonarr|radarr>, method POST, basic auth with the autopulse credentials, on Import and Upgrade (Rename/Delete too if wanted). Re-add them after any *arr restore.
  • First-deploy restore finds no snapshot. This is standard VolSync bootstrap: the ReplicationDestination provisions an empty PVC and the app initializes a fresh DB. No manual action.

Operational notes

  • Enable/disable lives in the namespace kustomization.yaml: the app entry - ./autopulse/ks.yaml is commented out when dormant. Keep it in alphabetical position when re-enabling.
  • Health endpoint /health (port 2875) is unauthenticated in v2, handy for a quick liveness check from inside the pod without credentials.
  • Verify a healthy deploy by checking, in order:
  • Kustomization autopulse reconciles Ready=True.
  • ExternalSecret autopulse-secret is SecretSynced=True and the rendered Secret carries a config.yaml with the auth block (a SecretSyncedError almost always means the 1Password item is in the wrong vault).
  • PVC autopulse is Bound (1Gi, ceph-block); ReplicationSources for NFS and remote both exist.
  • Pod is 1/1 Ready; logs show the SQLite DB created at /app/data/autopulse.db with no auth/config-load errors.