feat(qbittorrent): Intial commit. Not enabled.

This commit is contained in:
2026-02-27 14:10:57 +01:00
parent 1c0b6dc92b
commit 5e43dfc43d
12 changed files with 569 additions and 1 deletions

View File

@@ -15,7 +15,7 @@ spec:
namespace: observability
- name: storage-ready
namespace: flux-system
path: ./kubernetes/apps/media/kavita/app
path: ./kubernetes/apps/media/kavita
postBuild:
substitute:
APP: *app

View File

@@ -0,0 +1,28 @@
---
# yaml-language-server: $schema=https://schemas.tholinka.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: &app qbittorrent
namespace: &namespace media
spec:
interval: 1h
components:
- ../../../../components/volsync
- ../../../../components/keda/nfs-scaler
dependsOn:
- name: keda
namespace: observability
- name: storage-ready
namespace: flux-system
path: ./kubernetes/apps/media/qbittorrent/app
postBuild:
substitute:
APP: *app
VOLSYNC_CAPACITY: 2Gi
prune: true
sourceRef:
kind: GitRepository
name: flux-system
namespace: flux-system
targetNamespace: *namespace

View File

@@ -0,0 +1,86 @@
---
# yaml-language-server: $schema=https://schemas.tholinka.dev/external-secrets.io/externalsecret_v1.json
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: &secret qbittorrent
spec:
secretStoreRef:
kind: ClusterSecretStore
name: bitwarden
target:
name: *secret
template:
data:
GLUETUN_CONTROL_SERVER_API_KEY: '{{ .GLUETUN_API_KEY }}'
QBITTORRENT_HOST: qui.media.svc.cluster.local
# abusing the port sync script so it does what I want
QBITTORRENT_WEBUI_PORT: 80/proxy/{{ .GLUETUN_QB_PORT_SYNC_PROXY }}
dataFrom:
- extract:
key: gluetun
---
# yaml-language-server: $schema=https://schemas.tholinka.dev/external-secrets.io/externalsecret_v1.json
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: &secret wireguard
spec:
secretStoreRef:
kind: ClusterSecretStore
name: bitwarden
target:
name: *secret
template:
engineVersion: v2
data:
# wireguard
WIREGUARD_PRIVATE_KEY: '{{ .WIREGUARD_PRIVATE_KEY }}'
# WIREGUARD_ADDRESSES: '{{ .WIREGUARD_ADDRESSES }}' # no longer needed - default works fine
# openvpn
OPENVPN_USER: '{{ .OPENVPN_USER }}'
OPENVPN_PASSWORD: '{{ .OPENVPN_PASSWORD }}'
# service provider config
VPN_SERVICE_PROVIDER: '{{ .VPN_SERVICE_PROVIDER }}'
VPN_PORT_FORWARDING_PROVIDER: '{{ .VPN_SERVICE_PROVIDER }}'
UPDATER_VPN_SERVICE_PROVIDERS: '{{ .VPN_SERVICE_PROVIDER }}'
HEALTH_TARGET_ADDRESSES: '{{ .HEALTH_TARGET_ADDRESS }}'
SERVER_COUNTRIES: '{{ .SERVER_COUNTRIES }}'
SERVER_CITIES: '{{ .SERVER_CITIES }}'
# for healthchecks
GLUETUN_CONTROL_SERVER_API_KEY: '{{ .GLUETUN_API_KEY }}'
dataFrom:
- extract:
key: gluetun
---
# yaml-language-server: $schema=https://schemas.tholinka.dev/external-secrets.io/externalsecret_v1.json
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: &secret qbittorrent-gluetun
spec:
secretStoreRef:
kind: ClusterSecretStore
name: bitwarden
target:
name: *secret
creationPolicy: Owner
template:
data:
auth.toml: |
[[roles]]
name = "gluetun-qb-port-sync"
routes = [
"GET /v1/publicip/ip",
"GET /v1/portforward"
]
auth = "apikey"
apikey = "{{ .GLUETUN_API_KEY }}"
dataFrom:
- extract:
key: gluetun

View File

@@ -0,0 +1,265 @@
---
# yaml-language-server: $schema=https://raw.githubusercontent.com/bjw-s-labs/helm-charts/main/charts/other/app-template/schemas/helmrelease-helm-v2.schema.json
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: &app qbittorrent
spec:
interval: 1h
chartRef:
kind: OCIRepository
name: app-template
driftDetection:
ignore:
- paths: [/spec/replicas]
values:
controllers:
*app :
annotations:
reloader.stakater.com/auto: 'true'
initContainers:
gluetun:
image:
repository: ghcr.io/qdm12/gluetun
tag: v3.41.1@sha256:1a5bf4b4820a879cdf8d93d7ef0d2d963af56670c9ebff8981860b6804ebc8ab
env:
DNS_SERVER: 'off'
DNS_ADDRESS: 10.96.0.10 # coredns is either pihole, dnsmasq, or cloudflare over tls, so it's fine to use
HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH: &gluetunAuthPath /gluetun/auth.toml
FIREWALL_INPUT_PORTS: 80,8388,9999 # 80: WebUI, 8388 Socks Proxy, 9999 Kube Probes
FIREWALL_OUTBOUND_SUBNETS: 10.69.0.0/16,10.96.0.0/16 # Allow access to k8s subnets
HEALTH_SERVER_ADDRESS: :9999
HEALTH_SUCCESS_WAIT_DURATION: 10s
VPN_TYPE: wireguard
# VPN_TYPE: openvpn
VPN_PORT_FORWARDING: 'on'
PORT_FORWARD_ONLY: 'on'
VPN_INTERFACE: tun0
UPDATER_PERIOD: 24h
# slightly larger MTU
WIREGUARD_MTU: 1400
OPENVPN_MSSFIX: 1320
envFrom:
- secretRef:
name: wireguard
probes:
liveness:
enabled: true
custom: true
spec: &probe
exec:
command:
- /bin/sh
- -c
- |-
port="$(wget -O - -q --header="X-API-KEY: $GLUETUN_CONTROL_SERVER_API_KEY" http://localhost:8000/v1/portforward)"
echo $port
[ "$port" = '{"port":0}' ] && exit 1 || exit 0
# initialDelaySeconds: 5
# periodSeconds: 60
# successThreshold: 1
# timeoutSeconds: 30
# failureThreshold: 10
# randomly gluetun will fail to get public_ip on startup for some reason
# startup:
# enabled: true
# custom: true
# spec:
# <<: *probe
# exec:
# command:
# - /bin/sh
# - -c
# - |-
# ip="$(wget -O - -q --header="X-API-KEY: $GLUETUN_CONTROL_SERVER_API_KEY" http://localhost:8000/v1/publicip/ip)"
# echo $ip
# [ "$ip" = '{"public_ip":""}' ] && exit 1 || exit 0
# periodSeconds: 10
lifecycle:
postStart:
exec:
command:
- /bin/sh
- -c
- >-
(ip rule del table 51820; ip -6 rule del table 51820) || true
restartPolicy: Always
securityContext:
# can't be non-root, or it has no access to tunnel
runAsNonRoot: false
runAsUser: 0
runAsGroup: 0
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false # wants to create a user, etc
capabilities:
add: [NET_ADMIN]
resources:
requests:
cpu: 20m
limits:
memory: 100Mi
# TODO: Replace once gluetun supports socks5, nothing supports shadowsocks
socks5:
restartPolicy: Always
image:
repository: docker.io/serjs/go-socks5-proxy
tag: latest@sha256:0af522996f402c03ecd985a87997158eabeb28935365e3a384df37eafcf740ea
env:
PROXY_PORT: &proxy-port 8388
REQUIRE_AUTH: 'false'
resources:
requests:
cpu: 5m
limits:
memory: 32Mi
securityContext: &securityContext
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: [ALL] }
containers:
app:
image:
repository: ghcr.io/home-operations/qbittorrent
tag: 5.1.4@sha256:56ecd30a7f5e1357daf5eea2a6935134d329b7b268a3a9b9c67041b65fb87fc6
env:
UMASK: '022'
QBT_WEBUI_PORT: &port 80
PUSHOVER_ENABLED: true
envFrom:
- secretRef:
name: *app
probes:
startup:
enabled: true
spec:
httpGet:
path: /api/v2/app/version
port: *port
failureThreshold: 30
periodSeconds: 10
timeoutSeconds: 5
securityContext: *securityContext
resources:
requests:
cpu: 100m
limits:
memory: 8Gi
port-forward:
image:
repository: ghcr.io/tholinka/gluetun-qb-port-sync
tag: 0.0.6@sha256:a6405cc2e84cc553cfb0b7796d06468ed9f6d6363690b7f656bf0e10b134346d
env:
GLUETUN_CONTROL_SERVER_HOST: localhost
GLUETUN_CONTROL_SERVER_PORT: 8000
CRON_ENABLED: true
CRON_SCHEDULE: '*/5 * * * *'
LOG_TIMESTAMP: false
envFrom:
- secretRef:
name: *app
resources:
requests:
cpu: 5m
limits:
memory: 32Mi
securityContext: *securityContext
defaultPodOptions:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
service:
app:
primary: true
forceRename: *app
ports:
http:
primary: true
port: *port
gluetun:
suffix: gluetun
ports:
socks-proxy:
port: *proxy-port
route:
app:
hostnames:
- qb.tholinka.dev
parentRefs:
- name: envoy-internal
namespace: network
rules:
- backendRefs: [{ identifier: app }]
configMaps:
config:
data:
.qbt.toml: |
[qbittorrent]
addr = "http://localhost:80"
persistence:
config:
existingClaim: *app
advancedMounts:
*app :
app:
- path: /config
config-file:
type: configMap
name: *app
advancedMounts:
*app :
app:
- path: /config/.qbt.toml
subPath: .qbt.toml
readOnly: true
tmpfs:
type: emptyDir
advancedMounts:
*app :
port-forward:
- path: /config
subPath: config
app:
- path: /addons
subPath: addons
vuetorrent:
- path: /addons
subPath: addons
gluetun-auth:
type: secret
name: '{{ .Release.Name }}-gluetun'
advancedMounts:
*app :
gluetun:
- path: *gluetunAuthPath
subPath: auth.toml
media:
type: nfs
server: nas.servers.internal
path: /media
advancedMounts:
*app :
app:
- path: /media/downloads/qbittorrent
subPath: downloads/qbittorrent
tunnel:
type: hostPath
hostPath: /dev/net/tun
hostPathType: CharDevice
advancedMounts:
*app :
gluetun:
- path: /dev/net/tun

View File

@@ -0,0 +1,7 @@
---
# yaml-language-server: $schema=https://json.schemastore.org/kustomization
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./externalsecret.yaml
- ./helmrelease.yaml

View File

@@ -0,0 +1,6 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./app.ks.yaml

View File

@@ -0,0 +1,30 @@
---
# yaml-language-server: $schema=https://schemas.tholinka.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: &app qui
namespace: &namespace media
spec:
interval: 1h
components:
- ../../../../components/volsync
- ../../../../components/keda/nfs-scaler
dependsOn:
- name: keda
namespace: observability
- name: storage-ready
namespace: flux-system
- name: authentik # does not like the oidc provider not being available at startup
namespace: security
path: ./kubernetes/apps/media/qui/app
postBuild:
substitute:
APP: *app
VOLSYNC_CAPACITY: 2Gi
prune: true
sourceRef:
kind: GitRepository
name: flux-system
namespace: flux-system
targetNamespace: *namespace

View File

@@ -0,0 +1,21 @@
---
# yaml-language-server: $schema=https://schemas.tholinka.dev/external-secrets.io/externalsecret_v1.json
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: &secret qui
spec:
secretStoreRef:
kind: ClusterSecretStore
name: bitwarden
target:
name: *secret
template:
data:
QUI__SESSION_SECRET: '{{ .QUI_SESSION_SECRET }}'
QUI__OIDC_CLIENT_ID: '{{ .QUI_OIDC_CLIENT_ID }}'
QUI__OIDC_CLIENT_SECRET: '{{ .QUI_OIDC_CLIENT_SECRET }}'
dataFrom:
- extract:
key: *secret

View File

@@ -0,0 +1,87 @@
---
# yaml-language-server: $schema=https://raw.githubusercontent.com/bjw-s-labs/helm-charts/main/charts/other/app-template/schemas/helmrelease-helm-v2.schema.json
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: &app qui
spec:
interval: 1h
chartRef:
kind: OCIRepository
name: app-template
driftDetection:
ignore:
- paths: [/spec/replicas]
values:
controllers:
*app :
annotations:
reloader.stakater.com/auto: "true"
containers:
app:
image:
repository: ghcr.io/autobrr/qui
tag: v1.14.1@sha256:10b7945d4f0978f56a7cb939a011e1aeef3b8d500e825f409599ae754f95601b
env:
QUI__HOST: 0.0.0.0
QUI__PORT: &port 80
QUI__LOG_LEVEL: INFO
# oidc - make it true later
QUI__OIDC_ENABLED: false
# QUI__OIDC_ISSUER: https://auth.tholinka.dev/application/o/qui/
# QUI__OIDC_REDIRECT_URL: https://qui.tholinka.dev/api/auth/oidc/callback
# QUI__OIDC_DISABLE_BUILT_IN_LOGIN: true
envFrom:
- secretRef:
name: "{{ .Release.Name }}"
probes:
liveness: &probes
enabled: true
custom: true
spec:
httpGet:
path: /health
port: *port
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 1
failureThreshold: 3
readiness: *probes
resources:
requests:
cpu: 10m
limits:
memory: 2G
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
defaultPodOptions:
securityContext:
runAsNonRoot: true
runAsUser: 1003
runAsGroup: 1005
fsGroup: 1005
fsGroupChangePolicy: OnRootMismatch
service:
app:
ports:
http:
port: *port
route:
app:
hostnames:
- "{{ .Release.Name }}.laurivan.com"
parentRefs:
- name: envoy-internal
namespace: network
persistence:
config:
existingClaim: "{{ .Release.Name }}"
media:
type: nfs
server: 10.0.0.14
path: /mnt/Main/shares
globalMounts:
- path: /media

View File

@@ -0,0 +1,7 @@
---
# yaml-language-server: $schema=https://json.schemastore.org/kustomization
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./secrets.sops.yaml
- ./helmrelease.yaml

View File

@@ -0,0 +1,25 @@
apiVersion: v1
kind: Secret
metadata:
name: qui
stringData:
QUI__SESSION_SECRET: ENC[AES256_GCM,data:XkPD5CX2VLYplqSw9GQwrjAbJGmFOYKQzcCct2tOqMjb/IPxBaIuLQ==,iv:hLKHjRCtWngbSaxiOKzDKEDDLryfF4UIpn3gQieyQvw=,tag:B1xQBWWOTPWGj9kf4j+y5w==,type:str]
#ENC[AES256_GCM,data:DjPTDKJ5UEBDcA==,iv:zHbSdf/GChYY3i9cxTs4zZeV7ZIH3wOr4XCh/Fr6oGw=,tag:HbRQlXe5ZdDQbVHJkym4oQ==,type:comment]
QUI__OIDC_CLIENT_ID: ENC[AES256_GCM,data:hSioWKPmhJBWAy8=,iv:KVq82iF3iuVMyXl+w5raOgtDoyVklg0ddL4lE82r6yY=,tag:2gPa2F5cP36dMqL1qkP84w==,type:str]
QUI__OIDC_CLIENT_SECRET: ENC[AES256_GCM,data:LOv0jdRIV4BQiQU=,iv:lrKFDgUu42Ho3lZunsdoyayDPiQZKAgLRsLBXfD6d5M=,tag:ci3+Nr3G5PeK5YHx3fOE6w==,type:str]
sops:
age:
- recipient: age1yzrqhl9dk8ljswpmzsqme3enad5kxxhsptdvecy3lwlq0ms80gaqxrctst
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqRDRDT2JzZmF2RlcyREg5
aEgyZ0QwNTJQK2JYbDBrNjRhT3BNSzdFZGlzCndQVloyK1RUU281S1Q2YnI4eXQv
RVoxa0UxOFNEVkZwQzB3ZUhTNHBMTWcKLS0tIGZLMTZ3YUs3d2FHWVBtczJzdzhp
dUtWdGJ0cjhjREI5YnVzVDk5VGJJS0kKpa+N5XC8a5/V/eUgqZoosxrio9CJMTYS
TzhILOHxY59zNtl4Jw7QtIy27jWki4+318WnQ2XGHO5yPUitc1yPuA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-02-27T12:25:54Z"
mac: ENC[AES256_GCM,data:4Q60P6znbtKtC1SuRmFMR9b4fyoiL2rhBcxjepol5KWFqyg3+zF/fAbzB/Q3UeV4V3t0e+F0CfSlGP+e+ytv1F7EBJex7RhxWMiOSKsate3oOGoQEyW1GXq2YUS9Y8ZVuUHxdi4b2qpGeSJlSyL+mLs32nC9DorzDqyyOHjpZZ4=,iv:emjMwt6NX1LgRJjqaxEd2u2QTTYGxqletxxliSVnr8I=,tag:q/kTIXTXsWig/Lhb4uIczA==,type:str]
encrypted_regex: ^(data|stringData)$
mac_only_encrypted: true
version: 3.11.0

View File

@@ -0,0 +1,6 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./app.ks.yaml