adr/apps/adr-008-prowlarr.md
Rama main · réplica de lectura del repositorio original.
# Prowlarr Charm Implementation (Indexer Manager)
## Context and Problem Statement
Charmarr requires a Prowlarr charm to serve as the central indexer manager, synchronizing indexers to media managers (Radarr, Sonarr, Lidarr). Prowlarr is architecturally different from the media manager charms: it's a **provider** on the `media-indexer` interface rather than a requirer, and it doesn't need download client or media storage relations. We need to define implementation details including reconciler pattern, application registration, VPN handling, credential management, and scaling constraints.
**Key insight:** Prowlarr's role is to register arr apps as "Applications" via its API. Once registered, Prowlarr handles indexer synchronization internally - the charm doesn't trigger sync operations.
## Considered Options
### VPN Relation
* **Option 1:** Require VPN relation (mandatory)
* **Option 2:** Optional VPN relation with warning if absent
* **Option 3:** No VPN support
### Application Registration Approach
* **Option 1:** Charm triggers sync API after registering applications
* **Option 2:** Set `syncLevel: fullSync`, let Prowlarr handle sync internally
* **Option 3:** Manual sync only via Prowlarr UI
### Tag Management
* **Option 1:** Implement tag-based indexer filtering in v1
* **Option 2:** Skip tags, all indexers sync to all applications
* **Option 3:** User-configurable tags via charm config
### API Client Architecture
* **Option 1:** Extend existing `ArrApiClient` with Prowlarr methods
* **Option 2:** Separate `ProwlarrApiClient` with shared base class
* **Option 3:** Completely separate client, no code sharing
### Unit FQDN Discovery
* **Option 1:** Construct FQDN manually from model name and unit ordinal
* **Option 2:** Use `socket.getfqdn()` which returns headless service FQDN
## Decision Outcome
### VPN: Option 2 - Optional with warning
**Rationale:** Trash Guides recommends VPN only for torrent clients, not arr apps. Most indexers work fine without VPN. Edge case of `*.prowlarr.com` blocking VPN IPs (for indexer definitions) is rare. Users who need VPN can relate; those who don't aren't forced into unnecessary complexity.
### Application Registration: Option 2 - Set syncLevel, let Prowlarr handle it
**Rationale:** Prowlarr's internal sync mechanism is robust and handles edge cases (indexer updates, failures, retries). The charm's job is to register applications with `syncLevel: fullSync` - Prowlarr does the rest. Simpler charm code, proven sync logic.
### Tags: Option 2 - Skip for v1
**Rationale:** Quality profiles in each arr instance handle filtering. radarr-1080p won't grab 4K content even if indexer returns it. Tag management adds complexity without clear benefit for typical deployments. Users can configure tags manually in Prowlarr UI if needed.
### API Client: Option 2 - Separate client with shared base
**Decision:** Use `ProwlarrApiClient` which extends `BaseArrApiClient`, alongside `ArrApiClient` for Radarr/Sonarr/Lidarr. Both live in charmarr-lib.
- `BaseArrApiClient`: Shared HTTP mechanics (get, post, put, delete)
- `ArrApiClient`: Radarr/Sonarr/Lidarr endpoints (`/api/v3`)
- `ProwlarrApiClient`: Prowlarr endpoints (`/api/v1`)
**Rationale:** Clean separation - Prowlarr uses `/api/v1`, arr apps use `/api/v3`. Shared base avoids duplicating HTTP mechanics. No method pollution between clients.
See [lib/adr-001-shared-arr-code](../lib/adr-001-shared-arr-code.md) for full API client architecture and implementation details.
### Unit FQDN: Option 2 - Use `socket.getfqdn()`
**Rationale:** In a Juju K8s StatefulSet, `socket.getfqdn()` returns the pod's FQDN via the headless service (e.g., `prowlarr-0.prowlarr-endpoints.charmarr.svc.cluster.local`). No manual construction needed. This FQDN routes directly to the specific pod, avoiding load balancing issues.
## Implementation Details
### Reconciler Flow
```mermaid
flowchart TD
Start([Reconcile Event]) --> P1{Phase 1:<br/>Container Connected?}
P1 -->|No| W1[WaitingStatus:<br/>Waiting for Pebble]
P1 -->|Yes| P2{Phase 2:<br/>VPN Relation?}
P2 -->|Yes, Ready| P2a[Patch StatefulSet<br/>for VPN routing]
P2 -->|Yes, Not Ready| W2[WaitingStatus:<br/>Waiting for VPN]
P2 -->|No| P2b[Log warning:<br/>VPN recommended for some indexers]
P2a --> P3
P2b --> P3
P3[Phase 3:<br/>Configure Pebble Layer]
P3 --> P4{Phase 4:<br/>Workload Ready?<br/>/ping check}
P4 -->|No| W3[WaitingStatus:<br/>Starting Prowlarr]
P4 -->|Yes| P5{Phase 5:<br/>Is Leader?}
P5 -->|No| Done([ActiveStatus])
P5 -->|Yes| P6[Phase 6:<br/>Read API Key<br/>Store in Juju Secret]
P6 --> P7{Phase 7:<br/>Ingress Ready?}
P7 -->|Yes| P7a[Configure External URL]
P7 -->|No| P8
P7a --> P8
P8[Phase 8:<br/>Reconcile Applications<br/>from media-indexer relations]
P8 --> P9[Phase 9:<br/>Publish Provider Data<br/>to media-indexer relations]
P9 --> P10[Phase 10:<br/>Submit Ingress Config]
P10 --> Done
style P1 fill:#e1f5ff
style P2 fill:#e1f5ff
style P4 fill:#e1f5ff
style P5 fill:#e1f5ff
style P7 fill:#e1f5ff
style W1 fill:#fff9c4
style W2 fill:#fff9c4
style W3 fill:#fff9c4
style Done fill:#e8f5e9
```
### Phase 8: Application Reconciliation (Core Logic)
```python
def _reconcile_applications(self) -> None:
"""Reconcile Prowlarr applications from media-indexer relations.
AGGRESSIVE: Deletes any application not present in relations.
"""
# 1. Get current applications from Prowlarr
current_apps = self._api_client.get_applications()
current_by_name = {app["name"]: app for app in current_apps}
# 2. Build desired applications from relations
desired_apps: dict[str, dict] = {}
for requirer in self.indexer_provider.get_requirers():
config = ApplicationConfigBuilder.from_requirer_data(
requirer=requirer,
prowlarr_url=self._get_prowlarr_url(),
model=self.model,
)
desired_apps[requirer.instance_name] = config
# 3. Delete removed (AGGRESSIVE)
for name, current_app in current_by_name.items():
if name not in desired_apps:
logger.info(f"Removing application: {name}")
self._api_client.delete_application(current_app["id"])
# 4. Add or update
for name, app_config in desired_apps.items():
existing = current_by_name.get(name)
if existing:
if self._needs_update(existing, app_config):
logger.info(f"Updating application: {name}")
self._api_client.update_application(existing["id"], app_config)
else:
logger.info(f"Adding application: {name}")
self._api_client.add_application(app_config)
```
### ApplicationConfigBuilder
See [lib/adr-001-shared-arr-code](../lib/adr-001-shared-arr-code.md) for full `ApplicationConfigBuilder` implementation.
Transforms `MediaIndexerRequirerData` into Prowlarr application API payloads, handling:
- Media manager to implementation/configContract mapping
- API key retrieval from Juju secrets
- Base URL construction
### ProwlarrApiClient
See [lib/adr-001-shared-arr-code](../lib/adr-001-shared-arr-code.md) for full `ProwlarrApiClient` implementation.
Key methods:
- `get_applications()` / `add_application()` / `update_application()` / `delete_application()`
- `get_host_config()` / `update_host_config()`
- `get_indexers()` (read-only)
### Pebble Layer
Uses the Pebble/LinuxServer.io pattern from [ADR-015](adr-015-pebble-linuxserver-pattern.md):
- Run Prowlarr binary directly: `/app/bin/Prowlarr -nobrowser -data=/config`
- Hardcoded `user-id: 1000` / `group-id: 1000` (no storage relation, see ADR-015)
Prowlarr-specific environment:
- `PROWLARR__LOG__LEVEL` - from charm config
- `PROWLARR__APP__INSTANCENAME` - from app name
### Provider Data Publication
```python
def _publish_provider_data(self) -> None:
"""Publish Prowlarr's connection info to all media-indexer relations."""
# Use socket.getfqdn() to get unit's FQDN via headless service
unit_fqdn = socket.getfqdn()
api_url = f"http://{unit_fqdn}:9696"
provider_data = MediaIndexerProviderData(
api_url=api_url,
api_key_secret_id=self._api_key_secret_id,
indexer=MediaIndexer.PROWLARR,
base_path=None,
)
self.indexer_provider.publish_data(provider_data)
```
## charmcraft.yaml
```yaml
name: prowlarr-k8s
type: charm
title: Prowlarr
summary: Indexer manager for Usenet and BitTorrent
description: |
Prowlarr is an indexer manager/proxy for Usenet and BitTorrent.
This charm provides:
- Automatic registration of media managers (Radarr, Sonarr, Lidarr)
- Full indexer synchronization to connected applications
- Optional VPN integration for privacy
- Juju-native credential management
Users add indexers via Prowlarr UI. The charm handles application
registration and synchronization automatically.
links:
documentation: https://github.com/charmarr/prowlarr-k8s
source: https://github.com/charmarr/prowlarr-k8s
issues: https://github.com/charmarr/prowlarr-k8s/issues
assumes:
- k8s-api
- juju >= 3.6
platforms:
amd64:
- name: ubuntu
channel: "24.04"
parts:
charm:
source: .
plugin: uv
build-packages: [git]
build-snaps: [astral-uv]
containers:
prowlarr:
resource: prowlarr-image
resources:
prowlarr-image:
type: oci-image
description: OCI image for Prowlarr (LinuxServer)
upstream-source: lscr.io/linuxserver/prowlarr:latest
storage:
config:
type: filesystem
location: /config
minimum-size: 1G
provides:
media-indexer:
interface: media-indexer
requires:
ingress:
interface: ingress
limit: 1
optional: true
vpn-gateway:
interface: vpn-gateway
limit: 1
optional: true
config:
options:
sync-level:
type: string
default: "full-sync"
description: |
Sync level for connected applications.
- full-sync: Keep apps fully in sync with Prowlarr (recommended)
- add-remove-only: Only add/remove indexers, don't update
- disabled: Don't auto-sync (manual only)
log-level:
type: string
default: "info"
description: |
Application log level.
One of: trace, debug, info, warn, error
actions:
sync-indexers:
description: |
Manually trigger indexer sync to all connected applications.
Useful after adding new indexers via Prowlarr UI.
rotate-api-key:
description: |
Rotate the Prowlarr API key.
Generates new key, restarts Prowlarr, updates Juju secret.
Connected applications update automatically via secret-changed event.
```
## Differences from Radarr/Sonarr Charms
| Aspect | Prowlarr | Radarr/Sonarr |
|--------|----------|---------------|
| media-indexer role | **Provider** | Requirer |
| download-client | Not needed | Required |
| media-storage | Not needed | Required |
| VPN | Optional | Not needed |
| Recyclarr | Not applicable | Optional integration |
| API version | `/api/v1` | `/api/v3` |
| Default port | 9696 | 7878 / 8989 |
| Core function | Register arr apps | Configure download clients |
## What Prowlarr Charm Does NOT Do
- **Manage indexers** - User adds indexers via Prowlarr UI
- **Trigger sync API** - Prowlarr handles sync internally based on `syncLevel`
- **Manage tags** - User configures tags manually if needed
- **Configure FlareSolverr** - Separate charm if needed (v2+)
## Consequences
### Good
* Clean separation from arr charm pattern (provider vs requirer)
* Simple reconciler - just register applications, Prowlarr does the rest
* `socket.getfqdn()` provides unit FQDN without manual construction
* VPN optional - most users don't need it
* No tag complexity in v1
* Shared API client infrastructure via charmarr-lib (see [lib/adr-001](../lib/adr-001-shared-arr-code.md))
### Bad
* No tag-based filtering in v1 (users must configure manually)
* No FlareSolverr integration in v1
* Aggressive reconciliation deletes manually-added applications
* Single unit constraint (see scaling ADR)
## Related MADRs
- [interfaces/adr-003-media-indexer](../interfaces/adr-003-media-indexer.md) - media-indexer interface
- [interfaces/adr-002-secret-management](../interfaces/adr-002-secret-management.md) - Juju secrets for API keys
- [lib/adr-001-shared-arr-code](../lib/adr-001-shared-arr-code.md) - Shared arr infrastructure
- [apps/adr-004-radarr-sonarr](./adr-004-radarr-sonarr.md) - Media manager charms (consumers)
- [apps/adr-012-app-scaling-v1](./adr-012-app-scaling-v1.md) - Scaling constraints (leader-only pattern)