Commit Graph

7 Commits

Author SHA1 Message Date
unkin-agent a4253f09cd Add cheeztv kids Jellyfin instance and fafflix kids mounts
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
cheeztv is a second, kids-only Jellyfin instance in the media project. Kids
titles need a separate, safe library front (cheeztv.unkin.net) while staying
resumable in the existing adult instance (fafflix).

- apps/base/cheeztv: mirror the jellyfin (fafflix) HA stack 1:1 - same
  jellyfin-ha:v0.1.3 fork image, 2-replica StatefulSet, RWX transcode PVC,
  Intel iGPU transcode, CNPG Postgres + PgBouncer, Valkey transcode store,
  k8up config backup, VMPodScrape. Media mounts subPath kids on the shared
  movies/tv CephFS subvolumes so cheeztv sees only the kids trees.
- Separate state: own namespace, config PVC, cheeztv-postgres cluster,
  cheeztv-valkey, and cnpg-cheeztv / cheeztv-config-backup buckets - nothing
  shared with fafflix.
- Ingress/DNS: cheeztv.unkin.net (internal Traefik + external-dns at
  198.18.200.4, cert-manager cheeztv-tls) plus the cluster hostname variant
  cheeztv.k8s.syd1.au.unkin.net, matching how fafflix and logviewer.unkin.net
  are wired.
- fafflix: add movies/kids and tvshows/kids subPath mounts alongside its
  existing media mounts so kids libraries are browsable/resumable there; its
  existing mounts, hostname and ingress are untouched.
- Register cheeztv in the media ApplicationSet generator and AppProject
  destinations.
2026-08-24 22:08:04 +10:00
unkin-agent 13a04a6ebc Export and scrape metrics from all redis/valkey instances (#398)
## Why

No redis/valkey instance managed in this repo exports metrics today — the six caches (artifactapi, authentik, litellm, gitea, netbox, jellyfin) are blind spots in VictoriaMetrics.

## What

- Enables the valkey-operator exporter sidecar on the jellyfin ValkeyCluster (`exporter.enabled: true`); overrides the operator's default bare-dockerhub image (`oliver006/redis_exporter:v1.80.0`) with the artifactapi-proxied pin. The operator manages a dedicated `_exporter` ACL user for it.
- Adds a `metrics-exporter` sidecar (`artifactapi.k8s.syd1.au.unkin.net/dockerhub/oliver006/redis_exporter:v1.89.0`, :9121, port name `metrics`, req 32Mi/25m lim 64Mi/100m) to the five raw Deployments: artifactapi/authentik/litellm redis and gitea/netbox valkey. gitea/netbox sidecars get the same restrictive securityContext as their valkey container.
- Adds the `monitoring.unkin.net/redis-exporter: "true"` opt-in pod label to all five pod templates.
- Adds `apps/base/observability/vmpodscrape-redis.yaml` with two VMPodScrapes (any namespace, port `metrics`): `redis-exporters` selecting the opt-in label, and `valkey-operator-clusters` selecting `app.kubernetes.io/managed-by: valkey-operator` + `app.kubernetes.io/component: valkey-node` — the ValkeyCluster CR has no pod-label passthrough (verified against the v0.5.0 CRD/source), so operator pods cannot carry the opt-in label.

## Notes

- No instance runs with `requirepass`/auth, so no REDIS_PASSWORD wiring is needed on the raw Deployments; the jellyfin exporter auth is operator-managed.
- Validated: `kustomize build` on every touched base + au-syd1 overlay (helm overlays with `--enable-helm`) and `kubeconform -strict -ignore-missing-schemas` (68 valid, 0 invalid).

Reviewed-on: #398
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 19:19:16 +10:00
unkin-agent 9450a70652 jellyfin: make Valkey HA (#375)
Jellyfin runs 2 replicas that coordinate distributed transcoding through the operator-managed `jellyfin-valkey` ValkeyCluster, but it was single-instance (shards:1, replicas:0) with no failover, so a node/pod loss would drop the shared transcode-lease state. This promotes that store to an HA topology.

Changes:
- Set replicas:2 (shards:1 unchanged) so the shard group is one primary + two replicas; losing the primary triggers an automatic in-cluster failover to a replica.
- Add scheduling.node.spread.shard.mode:Required so the three ValkeyNodes land on distinct hosts (kubernetes.io/hostname anti-affinity) and one host loss removes at most one node.
- Set podDisruptionBudget.mode:Cluster so the operator manages a quorum-aware PDB.
- Keep persistence omitted (emptyDir): lease state is ephemeral (30s TTLs), replication+failover already provide redundancy and a replaced node re-syncs from the primary, and an operator-managed PVC cannot carry the k8up.io/backup:"false" annotation the namespace k8up Schedule needs to skip in-use RWO volumes.
- Bump per-node memory limit to 512Mi (unchanged) / requests unchanged; resources are per ValkeyNode.

Connection string is unchanged: still cluster-mode via the single headless service `valkey-jellyfin-valkey:6379`; StackExchange.Redis seeds off it and auto-discovers topology and failovers.

kustomize build --enable-helm apps/overlays/au-syd1/jellyfin renders cleanly; pre-commit passes.

Reviewed-on: #375
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-16 07:34:15 +10:00
unkin-agent 9e1ccdb0a0 jellyfin: bump image to v0.1.3 (.NET 9 runtime fix) (#374)
## Why
jellyfin-ha v0.1.2 crashed on startup: the framework-dependent .NET 9 build could not find its runtime (`Microsoft.NETCore.App 9.0.0` required, `10.0.11` found), because the runtime image was pinned to a .NET 10 base. jellyfin-ha PR #7 realigned the runtime image to `mcr.microsoft.com/dotnet/aspnet:9.0` and v0.1.3 was released.

Verified before this bump: `docker run --entrypoint dotnet ...:v0.1.3 --list-runtimes` lists `Microsoft.NETCore.App 9.0.19`, and `/jellyfin/jellyfin --version` prints `Jellyfin.Server 10.11.7.0` with no framework-not-found error.

## Change
- Bump jellyfin image tag from v0.1.2 to v0.1.3 in `apps/base/jellyfin/statefulset.yaml`.

---------

Co-authored-by: unkin-agent <unkin-agent@git.unkin.net>
Reviewed-on: #374
Co-authored-by: Unkin Agent <unkin-agent@unkin.net>
Co-committed-by: Unkin Agent <unkin-agent@unkin.net>
2026-08-15 19:01:47 +10:00
unkin-agent bbfa4db652 jellyfin: bump image to v0.1.2 (#373)
Bumps the jellyfin StatefulSet image tag to v0.1.2.

v0.1.2 is the first jellyfin-ha image successfully pushed to the artifactapi docker-internal registry: pipeline #7 (tag v0.1.2) is green, the docker build+push step exited 0, and the image is pullable (tags/list shows v0.1.2, manifest GET returns 200). v0.1.0 and v0.1.1 tag pipelines failed the CA/TLS check before the plugin-image + buildkit_config fix landed (PR #6).

Changes:
- Set apps/base/jellyfin/statefulset.yaml jellyfin container image tag from v0.1.0 to v0.1.2 (tag only).

Validated with kustomize build --enable-helm apps/overlays/au-syd1/jellyfin and pre-commit.

---------

Co-authored-by: unkin-agent <unkin-agent@users.noreply.git.unkin.net>
Reviewed-on: #373
Co-authored-by: Unkin Agent <unkin-agent@unkin.net>
Co-committed-by: Unkin Agent <unkin-agent@unkin.net>
2026-08-15 18:36:25 +10:00
unkin-agent 40ba1730de jellyfin: replace redis Deployment with valkey-operator instance (#367)
## Why

Jellyfin depended on a self-managed `redis` Deployment for its distributed transcode-session store. Now that the valkey-operator is deployed cluster-wide, this hands that store to the operator so it is declaratively managed (rolling upgrades, health, future HA) instead of a hand-rolled Deployment.

## Changes

- Add `apps/base/jellyfin/valkey.yaml`: a `ValkeyCluster` (`valkey.io/v1alpha1`) named `jellyfin-valkey`, single instance (`shards: 1`, `replicas: 0`), image via the artifactapi dockerhub mirror (`valkey/valkey:9.0.0`), metrics exporter disabled, redis-parity resources.
- Omit persistence so `/data` is an `emptyDir`: the transcode-lease state is ephemeral, and the operator-managed PVC cannot carry the `k8up.io/backup: "false"` annotation the namespace-wide k8up Schedule needs to skip an in-use RWO volume (the old redis PVC carried it).
- Remove `redis-deployment.yaml`, `redis-service.yaml`, `redis-pvc.yaml` and drop them from `kustomization.yaml`; add `valkey.yaml`.
- Repoint `Jellyfin__TranscodeStore__RedisConnectionString` at the operator client Service `valkey-jellyfin-valkey:6379`, preserving `abortConnect=false` and `LeaseDurationSeconds`.

## Auth / TLS

None. This operator runs Valkey cluster-mode-enabled with `protected-mode no` and leaves the built-in `default` user passwordless (no `requirepass`), so clients connect unauthenticated over plaintext — parity with the previous open redis. StackExchange.Redis auto-discovers the single node via the headless Service. No `VaultStaticSecret` / plain Secret is introduced.

## Validation

- `kustomize build --enable-helm apps/overlays/au-syd1/jellyfin` renders cleanly: `ValkeyCluster` present, no redis Deployment/Service/PVC, connection string updated.
- pre-commit passes (incl. the plain-secrets guard).
- kubeconform: 10 valid / 0 invalid; the `ValkeyCluster` is skipped (CRD schema not vendored).

## Follow-up

HA (task 6) is intentionally out of scope: it will raise `replicas` (and/or `shards`) and add anti-affinity/topology spread. Enabling auth (ACL `default` user + password via `VaultStaticSecret`) and/or TLS can also be layered on later.

Reviewed-on: #367
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-15 14:54:05 +10:00
unkinben 6c8c0dd9e4 Deploy jellyfin-ha as a true-HA StatefulSet under a new media project (#237)
## Why

Deploy the jellyfin-ha fork as a genuine high-availability service rather than a
single replica, so its two headline capabilities can actually be exercised: the
Redis-coordinated distributed transcoding (a surviving pod resumes the HLS
segments of a pod that dies mid-stream) and the experimental PostgreSQL main
database (which is what lets more than one replica share the same library). It
lands in its own `jellyfin` namespace under a new `media` ArgoCD project.

## How

**Workload — StatefulSet, 2 replicas.** The Deployment becomes a StatefulSet so
each pod has a stable name. The fork's Redis transcode lease keys ownership on
`JELLYFIN_INSTANCE_ID`, which is set from the downward-API pod name, giving each
replica a unique, stable lease identity. Soft `podAntiAffinity` spreads the two
pods across nodes and a `PodDisruptionBudget` keeps `minAvailable: 1` through
drains and rollouts.

**Main database — CloudNativePG.** A CNPG trio in-namespace mirrors the litellm
pattern: a 3-instance `Cluster`, a PgBouncer `Pooler`, and Ceph RGW (barman) S3
backups to a dedicated `cnpg-jellyfin` bucket owned by a `cephrgw`
`ObjectStoreUser`. An init container writes `/config/config/database.xml`
selecting the fork's `Jellyfin-PostgreSQL` provider, and the connection string is
composed from the CNPG-generated `jellyfin-postgres-app` secret (username /
password / dbname) pointed at the pooler service — the password is never
rendered into the manifest. Library-item metadata therefore moves off SQLite;
metadata images, plugins, subtitles and config XML stay on `/config`.

**Storage.**
- `/config` is now a shared `ReadWriteMany` cephfs PVC (raid5, retain) so every
  replica reads/writes the same metadata and config.
- `/config/transcodes` — the fork's real transcode temp path — is a shared RWX
  PVC (raid5, delete). This is the load-bearing fix: takeover reads the dead
  pod's in-flight `.ts`/`.m3u8` segments off shared storage, so per-pod scratch
  would silently break it.
- `/cache` is per-pod via a `volumeClaimTemplate` (RWO).
- The media library stays a fresh, empty RWX PVC mounted read-only; populating
  it is out of scope.

**Hardware transcoding.** The container requests the `gpu.intel.com/i915` Intel
device-plugin resource (which pins the pod to a GPU-labelled node and injects the
DRI render node — no `/dev/dri` hostPath or privileged container) plus the
render/video supplemental groups. VA-API hardware acceleration is now on by
default: the `inject-config` init container seeds `/config/config/encoding.xml`
with `HardwareAccelerationType` `vaapi`, `EnableHardwareEncoding`, the injected
render node (`/dev/dri/renderD128`) and h264/hevc hardware decode, so transcodes
use the iGPU on first boot with no manual admin-UI step. Both seed files
(`database.xml`, `encoding.xml`) are written only when absent, so later admin
changes persisted to the shared RWX `/config` are never clobbered on restart.

**Networking.** The Gateway/HTTPRoute (traefik-internal,
`jellyfin.k8s.syd1.au.unkin.net`) is unchanged; the Service gains
`sessionAffinity: ClientIP` to keep a client pinned to one replica and reduce
transcode-session churn.

**Redis.** The in-namespace single-replica Redis stays as the transcode lease
store.

## Follow-up

UDP auto-discovery is disabled, but scheduled library scans still run on every
replica (redundant scans). Single-scanner leader election is a planned follow-up
pending a fork feature, tracked separately.

---------

Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-authored-by: Ben Vincent <neotheo@gmail.com>
Co-authored-by: Ben Vin <neotheo@gmail.com>
Reviewed-on: #237
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-15 13:38:59 +10:00