feat: update JDownloader deployment for VLAN 50 migration with enhanced network configuration and egress guard
Some checks failed
validate / lint (push) Failing after 1s

This commit is contained in:
Nik Afiq 2026-08-24 22:45:08 +09:00
parent 2f7ec5f5b1
commit 756fa120b3

View File

@ -1,6 +1,25 @@
# Apply: kubectl apply -f manifests/media/jdownloader.yaml # Apply: kubectl apply -f manifests/media/jdownloader.yaml
# Delete: kubectl delete -f manifests/media/jdownloader.yaml # Delete: kubectl delete -f manifests/media/jdownloader.yaml
# Description: JDownloader deployment with Ingress at jdownloader.home.arpa. # Description: JDownloader deployment with Ingress at jdownloader.home.arpa.
#
# *** VLAN 50 MIGRATION — DO NOT PUSH TO main WITHOUT SEPARATE, EXPLICIT
# APPROVAL — AND ONLY AFTER qBittorrent (manifests/media/
# qbittorrent.yaml) HAS BEEN LIVE, VALIDATED, AND SOAKED. plan.md is
# explicit: migrate qBittorrent first, soak, then JDownloader,
# independently soaked — do not batch them. ***
# Same VLAN 50/Multus design as qbittorrent.yaml (see that file's header
# for the full precondition list — pia-gateway, vlan50-parent, and
# manifests/multus/ all applied and verified first). One difference:
# JDownloader has no reliably-persistent, file-editable interface-bind
# setting the way qBittorrent's qBittorrent.conf does (its own
# preferences aren't a simple INI this repo can safely patch), so this
# migration relies on the network-namespace egress guard alone for
# enforcement, exactly as plan.md anticipates for JDownloader
# specifically ("application-level interface binding may be weaker or
# unavailable... the namespace egress guard is mandatory").
# `media` has selfHeal+automated sync already configured — pushing this
# file deploys it immediately. Rollback copy, not synced by any
# Application: manifests/media/rollback/jdownloader-gluetun.yaml.
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@ -17,66 +36,65 @@ spec:
metadata: metadata:
labels: labels:
app: jdownloader app: jdownloader
annotations:
k8s.v1.cni.cncf.io/networks: |
[{"name": "vlan50", "namespace": "downloads", "interface": "net1", "ips": ["10.10.50.11/24"]}]
spec: spec:
# Hard-pinned to nik-debian, not just node-role: storage — see
# qbittorrent.yaml for why.
nodeSelector: nodeSelector:
node-role: storage node-role: storage
containers: kubernetes.io/hostname: nik-debian
- name: gluetun # No cluster DNS/CoreDNS resolution needed or provided here — see
image: qmcgaw/gluetun:v3.41 # qbittorrent.yaml for the full reasoning (identical here: this pod
# never looks up an in-cluster service by name).
dnsPolicy: None
dnsConfig:
nameservers:
- "10.10.40.53"
initContainers:
# NET_ADMIN lives here ONLY. No qBittorrent-style config-bind
# sibling init container — see this file's header for why
# JDownloader relies on the egress guard alone.
- name: vlan50-egress-guard
image: nicolaka/netshoot:v0.11
command: ["/bin/sh", "/scripts/guard.sh"]
env:
- name: VLAN50_GATEWAY
value: "10.10.50.1"
- name: TECHNITIUM_IP
value: "10.10.40.53"
- name: POD_CIDR
value: "10.42.0.0/16"
- name: SERVICE_CIDR
value: "10.43.0.0/16"
- name: NODE_IP
value: "10.10.40.20"
- name: EXPECTED_VLAN50_IP
value: "10.10.50.11"
securityContext: securityContext:
capabilities: capabilities:
add: drop: ["ALL"]
- NET_ADMIN # NET_RAW alongside NET_ADMIN: the guard script's
env: # arping-based duplicate-address and gateway-reachability
- name: VPN_SERVICE_PROVIDER # checks need it — see
value: private internet access # vlan50-egress-guard-script.yaml's header comment.
- name: VPN_TYPE add: ["NET_ADMIN", "NET_RAW"]
value: openvpn
- name: SERVER_REGIONS
value: JP Tokyo,Taiwan,Hong Kong
# value: US Seattle,US Oregon,US Silicon Valley
- name: OPENVPN_USER
valueFrom:
secretKeyRef:
name: pia-credentials
key: OPENVPN_USER
- name: OPENVPN_PASSWORD
valueFrom:
secretKeyRef:
name: pia-credentials
key: OPENVPN_PASSWORD
- name: FIREWALL_OUTBOUND_SUBNETS
value: "10.42.0.0/16,10.43.0.0/16,10.10.40.0/24"
- name: BLOCK_IPV6
value: "on"
startupProbe:
exec:
command:
- /gluetun-entrypoint
- healthcheck
periodSeconds: 10
failureThreshold: 60
livenessProbe:
exec:
command:
- /gluetun-entrypoint
- healthcheck
periodSeconds: 30
failureThreshold: 3
readinessProbe:
exec:
command:
- /gluetun-entrypoint
- healthcheck
periodSeconds: 10
failureThreshold: 3
volumeMounts: volumeMounts:
- name: tun - name: guard-script
mountPath: /dev/net/tun mountPath: /scripts
containers:
- name: jdownloader - name: jdownloader
image: jlesage/jdownloader-2:latest image: jlesage/jdownloader-2:latest
ports: ports:
- containerPort: 5800 - containerPort: 5800
# No securityContext capability restriction here, deliberately
# — jlesage/jdownloader-2 uses the same docker-baseimage-gui
# PUID/GID pattern as linuxserver's images (USER_ID/GROUP_ID
# below): starts as root, its own init chowns /config to the
# requested uid/gid and drops privileges from there. See
# qbittorrent.yaml's app container for the fuller version of
# this reasoning — same conclusion, same kind of image.
env: env:
- name: USER_ID - name: USER_ID
value: "1000" value: "1000"
@ -112,6 +130,12 @@ spec:
image: python:3.12-alpine image: python:3.12-alpine
ports: ports:
- containerPort: 9667 - containerPort: 9667
# Unlike the jdownloader container above, this is a plain
# Python base image with no PUID/GID privilege-drop machinery
# to preserve — dropping all capabilities here is safe.
securityContext:
capabilities:
drop: ["ALL"]
resources: resources:
requests: requests:
cpu: 5m cpu: 5m
@ -149,8 +173,10 @@ spec:
# .torrent files aren't handled by JDownloader's crawler (no BT # .torrent files aren't handled by JDownloader's crawler (no BT
# plugin), so fetch the raw file ourselves instead of writing a # plugin), so fetch the raw file ourselves instead of writing a
# crawljob. This container shares gluetun's netns, so the fetch # crawljob. This container shares the pod's network namespace
# is still VPN-routed same as JDownloader's own downloads. # (and its vlan50-egress-guard init container's routes/rules),
# so the fetch is still VPN-routed same as JDownloader's own
# downloads.
if basename.lower().endswith('.torrent'): if basename.lower().endswith('.torrent'):
safe_name = basename.replace('/', '_').replace('\\', '_') safe_name = basename.replace('/', '_').replace('\\', '_')
try: try:
@ -183,10 +209,6 @@ spec:
http.server.HTTPServer(('0.0.0.0', 9667), Handler).serve_forever() http.server.HTTPServer(('0.0.0.0', 9667), Handler).serve_forever()
volumes: volumes:
- name: tun
hostPath:
path: /dev/net/tun
type: CharDevice
- name: config - name: config
hostPath: hostPath:
path: /data/jdownloader path: /data/jdownloader
@ -195,6 +217,10 @@ spec:
hostPath: hostPath:
path: /mnt/storage/dl path: /mnt/storage/dl
type: Directory type: Directory
- name: guard-script
configMap:
name: vlan50-egress-guard-script
defaultMode: 365 # octal 0555, r-xr-xr-x
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service