From 58eaef409f186aed7bb54b32d00c9ba359059715 Mon Sep 17 00:00:00 2001 From: Yuto Nishida Date: Sat, 3 Oct 2026 20:02:47 -0700 Subject: [PATCH] orion: deploy FlareSolverr and wire Prowlarr proxy --- .../stage00/orion-system/main.jsonnet | 39 ++++++++--- milky-way/lib/flaresolverr.libsonnet | 68 +++++++++++++++++++ milky-way/lib/images.libsonnet | 18 +++-- 3 files changed, 110 insertions(+), 15 deletions(-) create mode 100644 milky-way/lib/flaresolverr.libsonnet diff --git a/milky-way/environments/stage00/orion-system/main.jsonnet b/milky-way/environments/stage00/orion-system/main.jsonnet index 03b92ff..5593da9 100644 --- a/milky-way/environments/stage00/orion-system/main.jsonnet +++ b/milky-way/environments/stage00/orion-system/main.jsonnet @@ -19,6 +19,7 @@ local thelounge = import 'milky-way/lib/thelounge.libsonnet'; local sonarr = import 'milky-way/lib/sonarr.libsonnet'; local radarrForSdxarr = import 'milky-way/lib/radarr.libsonnet'; local prowlarr = import 'milky-way/lib/prowlarr.libsonnet'; +local flaresolverr = import 'milky-way/lib/flaresolverr.libsonnet'; local jellyfin = import 'milky-way/lib/jellyfin.libsonnet'; local seanime = import 'milky-way/lib/seanime.libsonnet'; local shoko = import 'milky-way/lib/shoko.libsonnet'; @@ -335,6 +336,10 @@ local pubkeys = import 'magic/common/public_keys.json'; tailscaleHostname = "prowlarr", ), + // In-cluster browser solver for Prowlarr indexers that need Cloudflare challenges resolved. + // Buildarr declares Prowlarr's proxy and tag below; select indexers by applying that tag. + flaresolverr: flaresolverr.new(), + // Jellyfin: media server for the library the *arr stack builds on the shared mdata volume. // Reads /data/library/... (same PVC, same /data mount as sonarr/qbittorrent), so it serves the // exact tree Sonarr hardlinks completed downloads into. SQLite config + metadata cache on its @@ -646,7 +651,10 @@ local pubkeys = import 'magic/common/public_keys.json'; prowlarr: { // GLOBAL default for all prowlarr instances (current + future). MUST stay false -- never // clobber apps/indexers added by hand in Prowlarr's UI. - settings: { apps: { applications: { delete_unmanaged: false } } }, + settings: { + apps: { applications: { delete_unmanaged: false } }, + indexers: { proxies: { delete_unmanaged: false } }, + }, instances: { [prowlarrOrionSystemInstanceName]: { hostname: utils.domainOfService(this.prowlarr.service), @@ -654,6 +662,25 @@ local pubkeys = import 'magic/common/public_keys.json'; protocol: 'http', api_key: secrets.prowlarr.apiKey, settings: { + tags: { definitions: ['flaresolverr'] }, + indexers: { + proxies: { + delete_unmanaged: false, + definitions: { + FlareSolverr: { + type: 'flaresolverr', + host_url: httpUrl( + utils.domainOfService(this.flaresolverr.service), + utils.associateObjectsByKey(this.flaresolverr.service.spec.ports, 'name')['http'].port, + ), + request_timeout: 60, + // Only indexers with this tag use FlareSolverr, and only when Prowlarr + // detects a Cloudflare challenge. Indexer selection stays manual for now. + tags: ['flaresolverr'], + }, + }, + }, + }, apps: { applications: { delete_unmanaged: false, // also explicit per-instance (belt & suspenders) @@ -681,9 +708,7 @@ local pubkeys = import 'magic/common/public_keys.json'; // importing. The app entry is KEPT (not deleted) so Buildarr still owns the // Prowlarr<->Sonarr link; only the indexer sync is turned off. (All of Sonarr's // other settings -- naming, root folders, download client -- come from the - // `sonarr:` instance block above, not from here.) NOTE: Buildarr's Prowlarr - // reconcile is currently broken (an empty-apikey indexer trips pydantic), so this - // was ALSO applied at runtime via the Prowlarr API. Flip back to 'full_sync' only + // `sonarr:` instance block above, not from here.) Flip back to 'full_sync' only // if this instance should ever search indexers on its own. sync_level: 'disabled', }, @@ -721,9 +746,7 @@ local pubkeys = import 'magic/common/public_keys.json'; // profile has upgradeAllowed=false, whichever release lands first wins forever, so an // on-its-own grab would permanently block SeaDexArr's curated release from ever // importing. The app entry is KEPT (not deleted) so Buildarr still owns the - // Prowlarr<->Radarr link; only the indexer sync is turned off. As with Sonarr, - // Buildarr's Prowlarr reconcile is currently broken (an empty-apikey indexer trips - // pydantic), so this was ALSO applied at runtime via the Prowlarr API. Flip to + // Prowlarr<->Radarr link; only the indexer sync is turned off. Flip to // 'full_sync' only if this instance should ever search indexers on its own. sync_level: 'disabled', }, @@ -920,4 +943,4 @@ local pubkeys = import 'magic/common/public_keys.json'; cilium: charts.cilium, traefikConfig: traefik.reconfigForCilium(), -} \ No newline at end of file +} diff --git a/milky-way/lib/flaresolverr.libsonnet b/milky-way/lib/flaresolverr.libsonnet new file mode 100644 index 0000000..ea0cccc --- /dev/null +++ b/milky-way/lib/flaresolverr.libsonnet @@ -0,0 +1,68 @@ +local images = import 'milky-way/lib/images.libsonnet'; +local utils = import 'milky-way/lib/utils.libsonnet'; + +// FlareSolverr runs a browser for Prowlarr's selected indexer requests. Prowlarr reaches it +// through the in-cluster Service; it has no persistent state or external ingress. +{ + new( + name='flaresolverr', + namespace='default', + image=images.flaresolverr.fullyQualifiedImageReferencePinned, + port=8191, + timezone='America/Los_Angeles', + ):: { + local this = self, + + deployment: { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { name: name, namespace: namespace }, + spec: { + replicas: 1, + selector: { matchLabels: { app: name } }, + template: { + metadata: { labels: {} + this.deployment.spec.selector.matchLabels }, + spec: { + tolerations: [{ key: 'ephemeral', operator: 'Exists', effect: 'NoSchedule' }], + containers: [{ + name: name, + image: image, + env: [ + { name: 'PORT', value: std.toString(port) }, + { name: 'TZ', value: timezone }, + { name: 'LOG_LEVEL', value: 'info' }, + ], + ports: [{ name: 'http', containerPort: port }], + readinessProbe: { + httpGet: { path: '/health', port: 'http' }, + initialDelaySeconds: 15, + periodSeconds: 15, + }, + resources: { + requests: { memory: '512Mi', cpu: '100m' }, + limits: { memory: '2Gi', cpu: '2' }, + }, + }], + }, + }, + }, + }, + + service: { + apiVersion: 'v1', + kind: 'Service', + metadata: { name: name, namespace: namespace }, + spec: { + selector: {} + this.deployment.spec.template.metadata.labels, + ports: [{ + name: 'http', + port: port, + targetPort: utils.assertEqualAndReturn( + this.deployment.spec.template.spec.containers[0].ports[0].name, 'http' + ), + }], + type: 'ClusterIP', + }, + }, + }, +} diff --git a/milky-way/lib/images.libsonnet b/milky-way/lib/images.libsonnet index c9f2d62..d6981cc 100644 --- a/milky-way/lib/images.libsonnet +++ b/milky-way/lib/images.libsonnet @@ -71,6 +71,12 @@ local images = { fullyQualifiedRepository: "lscr.io/linuxserver/prowlarr", defaultDigest: { hash: "sha256:7ab5769616c1929247c8e7944453253f0b777fac2724c3bc9976ae2ff4023257", tagHint: "2.4.0.5397-ls150" }, }, + // Browser-backed indexer challenge solver for Prowlarr. Multi-arch index digest from the + // upstream GitHub Container Registry package for v3.5.2. + flaresolverr: { + fullyQualifiedRepository: "ghcr.io/flaresolverr/flaresolverr", + defaultDigest: { hash: "sha256:c80ae007ce2ccdcd217a12426e4f039ef763ff90738c808d38810c3e59323767", tagHint: "v3.5.2" }, + }, // Jellyfin media server (LinuxServer.io): plays the library the *arr stack builds on the // shared mdata volume. Same multi-arch INDEX digest convention as the *arr/qbittorrent pins // above (k3s resolves the per-node arch); tagHint is the readable LinuxServer version. @@ -118,14 +124,12 @@ local images = { fullyQualifiedRepository: "lscr.io/linuxserver/thelounge", defaultDigest: { hash: "sha256:07f9dc09e4a781d4ee38a06378c183005b28b9872b98a8a31cfb4c315ba23fdc", tagHint: "v4.5.0-ls223" }, }, - // Buildarr: declaratively reconciles *arr state (used here only to wire Sonarr<->Prowlarr<-> - // qBittorrent together). The image bundles the sonarr/radarr/prowlarr plugins. The hash is the - // multi-arch INDEX digest (same as the *arr/qbittorrent pins above; k3s resolves the per-node - // arch); tagHint is the readable release. Re-resolve with - // `docker buildx imagetools inspect callum027/buildarr:latest`. + // Buildarr: upstream 0.7.8 with a one-file fix for buildarr-prowlarr 0.5.3's empty API-key + // textbox validation (see whale/outputs.nix and its patch). Methanol is linux/amd64, matching + // the whale-built image. Digest is written by `nix run ./flake-profiles/whale#buildarr-push`. buildarr: { - fullyQualifiedRepository: "callum027/buildarr", - defaultDigest: { hash: "sha256:57e2343fefe5d5701364b5e93b4985dbf08310d7b152f70556bdaba7e9475447", tagHint: "0.7.8" }, + fullyQualifiedRepository: "docker.io/yuto7/buildarr", + defaultDigest: { hash: std.trim(importstr "exports/whale/digests/buildarr.txt") }, }, // SeaDexArr (bbtufty): scheduled daemon syncing Sonarr/Radarr anime picks from SeaDex into // qBittorrent. WHALE-BUILT FORK of the pinned upstream `:main` image + a one-line patch to -- 2.51.2