--- apiVersion: apps/v1 kind: Deployment metadata: name: qbittorrentvpn namespace: plex spec: strategy: type: Recreate selector: matchLabels: app: qbittorrentvpn replicas: 1 template: metadata: labels: app: qbittorrentvpn annotations: backup.velero.io/backup-volumes-excludes: seedbox,media,media2,data-ec,scratch spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: seedbox operator: In values: - "true" tolerations: - key: seedbox operator: Equal value: "true" effect: NoSchedule containers: - name: qbittorrentvpn image: binhex/arch-qbittorrentvpn:5.1.0-1-01 ports: - containerPort: 8080 name: http-web-svc securityContext: privileged: true envFrom: - secretRef: name: qbittorrentvpn-secret livenessProbe: exec: command: ["curl", "--fail", "localhost:8080"] volumeMounts: - mountPath: "/data" name: seedbox - mountPath: "/media" name: media - mountPath: "/media2" name: media2 - mountPath: "/dataec" name: data-ec - mountPath: "/config" name: config - mountPath: "/scratch" name: scratch volumes: - name: seedbox persistentVolumeClaim: claimName: seedbox-pvc - name: media persistentVolumeClaim: claimName: plex-pvc - name: media2 persistentVolumeClaim: claimName: media2-pvc - name: data-ec persistentVolumeClaim: claimName: data-ec-pvc - name: config persistentVolumeClaim: claimName: qbittorrentvpn-pvc - name: scratch hostPath: path: /mnt/data/torrents type: Directory --- apiVersion: apps/v1 kind: Deployment metadata: name: qbittorrentvpn-exporter namespace: plex spec: strategy: type: Recreate selector: matchLabels: app: qbittorrentvpn-exporter replicas: 1 template: metadata: labels: app: qbittorrentvpn-exporter spec: containers: - name: qbittorrentvpn-exporter image: ghcr.io/esanchezm/prometheus-qbittorrent-exporter:v1.6.0 ports: - containerPort: 8000 name: metrics envFrom: - secretRef: name: qbittorrentvpn-exporter-secret livenessProbe: exec: command: - "/bin/sh" - "-c" - 'wget -O - 0.0.0.0:8000 | grep -E "qbittorrent_up\{.* 1.0"' initialDelaySeconds: 3 timeoutSeconds: 5 periodSeconds: 3 resources: requests: memory: "0" limits: memory: "256Mi" --- apiVersion: v1 kind: Service metadata: name: qbittorrentvpn-service namespace: plex spec: selector: app: qbittorrentvpn type: ClusterIP ports: - name: qbittorrentvpn-web-port protocol: TCP port: 8080 targetPort: http-web-svc --- apiVersion: v1 kind: Service metadata: name: qbittorrentvpn-exporter-service namespace: plex labels: app: qbittorrentvpn-exporter spec: selector: app: qbittorrentvpn-exporter type: ClusterIP ports: - name: metrics protocol: TCP port: 8000 targetPort: metrics --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: qbittorrentvpn namespace: plex annotations: traefik.ingress.kubernetes.io/router.entrypoints: websecure traefik.ingress.kubernetes.io/router.middlewares: kube-system-lanonly@kubernetescrd spec: rules: - host: qbittorrentvpn.lan.jibby.org http: paths: - path: / pathType: Prefix backend: service: name: qbittorrentvpn-service port: number: 8080 --- apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: labels: prometheus: qbittorrent role: alert-rules name: prometheus-qbittorrent-rules namespace: plex spec: groups: - name: ./qbittorrent.rules rules: - alert: QbittorrentErroredTorrents expr: sum(qbittorrent_torrents_count{status="error"}) > 0 --- apiVersion: batch/v1 kind: CronJob metadata: name: qbittorrentvpn-restart namespace: plex spec: schedule: "*/30 * * * *" successfulJobsHistoryLimit: 1 failedJobsHistoryLimit: 1 concurrencyPolicy: Forbid jobTemplate: spec: template: metadata: labels: app: qbittorrentvpn-restart spec: serviceAccountName: qbittorrentvpn-restart-serviceaccount securityContext: runAsUser: 1000 runAsGroup: 1000 restartPolicy: OnFailure containers: - name: qbittorrentvpn-restart image: python:3.14 command: - python3 - -c - | import subprocess import json import pprint import urllib.parse import sys import datetime # Vars to configure namespace = 'plex' qparams = {'labelSelector': 'app=qbittorrentvpn'} max_runtime = datetime.timedelta(days=1) # serviceaccount/k8s specific vars. Likely don't need to edit these. serviceaccount_dir = '/var/run/secrets/kubernetes.io/serviceaccount' apiserver = 'https://kubernetes.default.svc' token = open(f'{serviceaccount_dir}/token').read() result = subprocess.run([ 'curl', '--cacert', f'{serviceaccount_dir}/ca.crt', '--header', f'Authorization: Bearer {token}', '-X', 'GET', f'{apiserver}/api/v1/namespaces/{namespace}/pods?{urllib.parse.urlencode(qparams)}' ], capture_output=True, check=True, ) pod_list = json.loads(result.stdout) items = pod_list.get('items') if items is None or len(items) < 1: print(f'No pod found? Exiting. {pod_list=}') sys.exit(1) if len(items) > 1: print(f'>1 pod? Exiting. {items=}, {len(items)=}') sys.exit(1) pod = items[0] container_statuses = pod['status']['containerStatuses'] if len(container_statuses) != 1: print(f'len(containerStatuses) != 1? Exiting. {container_statuses=}') sys.exit(1) running = container_statuses[0]['state'].get('running') if not running: print(f'Pod not running? Exiting. {container_statuses["state"]=}') started_at = datetime.datetime.fromisoformat(running["startedAt"]) runtime = datetime.datetime.now(tz=datetime.UTC) - started_at print(f'{runtime=} > {max_runtime=} ? {runtime > max_runtime}') if runtime > max_runtime: pod_name = pod['metadata']['name'] print(f'Deleting pod {pod_name}') result = subprocess.run([ 'curl', '--cacert', f'{serviceaccount_dir}/ca.crt', '--header', f'Authorization: Bearer {token}', '-X', 'DELETE', f'{apiserver}/api/v1/namespaces/{namespace}/pods/{pod_name}' ], capture_output=True, check=True, ) --- apiVersion: v1 kind: ServiceAccount metadata: name: qbittorrentvpn-restart-serviceaccount namespace: plex --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: qbittorrentvpn-restart-serviceaccount-edit namespace: plex roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: edit subjects: - kind: ServiceAccount name: qbittorrentvpn-restart-serviceaccount namespace: plex