P6: Add Kiwix and Transmission k8s manifests

- Add SMB CSI driver ArgoCD application for mounting sifaka share
- Add transmission deployment in torrent namespace with Tailscale ingress
- Add kiwix deployment with torrent-sync sidecar for declarative ZIM management
- Add zim-watcher CronJob to auto-restart kiwix when new ZIMs complete
- Both services share SMB PV mounted from sifaka:/volume1/torrents

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-01-20 18:49:09 -08:00
commit 1c3c07187a
19 changed files with 607 additions and 0 deletions

18
argocd/apps/kiwix.yaml Normal file
View file

@ -0,0 +1,18 @@
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: kiwix
namespace: argocd
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/kiwix
destination:
server: https://kubernetes.default.svc
namespace: kiwix
syncPolicy:
syncOptions:
- CreateNamespace=true

27
argocd/apps/smb-csi.yaml Normal file
View file

@ -0,0 +1,27 @@
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: smb-csi
namespace: argocd
spec:
project: default
sources:
# Helm chart from forge mirror
- repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/csi-driver-smb.git
targetRevision: v1.17.0
path: charts/csi-driver-smb
helm:
releaseName: csi-driver-smb
valueFiles:
- $values/argocd/manifests/smb-csi/values.yaml
# Values from our git repo
- repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
targetRevision: main
ref: values
destination:
server: https://kubernetes.default.svc
namespace: kube-system
syncPolicy:
syncOptions:
- CreateNamespace=true

18
argocd/apps/torrent.yaml Normal file
View file

@ -0,0 +1,18 @@
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: torrent
namespace: argocd
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/torrent
destination:
server: https://kubernetes.default.svc
namespace: torrent
syncPolicy:
syncOptions:
- CreateNamespace=true

View file

@ -0,0 +1,67 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: zim-torrent-sync-script
namespace: kiwix
data:
sync-zim-torrents.sh: |
#!/bin/bash
# Sync ZIM torrents from kiwix ConfigMap to Transmission
# Runs as a sidecar in the kiwix deployment
set -euo pipefail
TORRENT_LIST="${TORRENT_LIST:-/config/torrents.txt}"
TRANSMISSION_HOST="${TRANSMISSION_HOST:-transmission.torrent.svc.cluster.local}"
TRANSMISSION_PORT="${TRANSMISSION_PORT:-9091}"
echo "Syncing ZIM torrents to transmission at ${TRANSMISSION_HOST}:${TRANSMISSION_PORT}"
# Wait for transmission to be ready
echo "Waiting for Transmission RPC..."
max_attempts=30
attempt=0
until curl -sf "http://${TRANSMISSION_HOST}:${TRANSMISSION_PORT}/transmission/rpc" >/dev/null 2>&1; do
attempt=$((attempt + 1))
if [[ $attempt -ge $max_attempts ]]; then
echo "Transmission not ready after ${max_attempts} attempts, will retry next cycle"
exit 0 # Don't fail, just skip this sync
fi
sleep 10
done
echo "Transmission is ready"
# Get current torrents from transmission
# transmission-remote returns header + data + footer, extract just torrent names
current=$(transmission-remote "${TRANSMISSION_HOST}:${TRANSMISSION_PORT}" -l 2>/dev/null | \
tail -n +2 | head -n -1 | awk '{print $NF}' || true)
added=0
skipped=0
while IFS= read -r url || [[ -n "$url" ]]; do
# Skip empty lines and comments
[[ -z "$url" || "$url" =~ ^[[:space:]]*# ]] && continue
# Trim whitespace
url=$(echo "$url" | xargs)
[[ -z "$url" ]] && continue
# Extract base name from URL (remove .torrent extension)
basename=$(basename "$url" .torrent)
# Also try without .zim in case transmission reports it differently
basename_no_zim="${basename%.zim}"
# Check if already in transmission
if echo "$current" | grep -qF "$basename_no_zim"; then
((skipped++)) || true
else
if transmission-remote "${TRANSMISSION_HOST}:${TRANSMISSION_PORT}" -a "$url" 2>/dev/null; then
echo "Added: $basename"
((added++)) || true
else
echo "Warning: Failed to add $url" >&2
fi
fi
done < "$TORRENT_LIST"
echo "Sync complete: $added added, $skipped already present"

View file

@ -0,0 +1,69 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: kiwix-zim-torrents
namespace: kiwix
data:
torrents.txt: |
# Declarative ZIM archive torrent URLs
# These are synced to transmission automatically by the kiwix sidecar
# Format: one URL per line, comments start with #
#
# Users can also add ZIM torrents manually via torrent.tail8d86e.ts.net
# and kiwix will pick them up automatically.
# Wikipedia - Top 1M English articles (43G)
https://download.kiwix.org/zim/wikipedia/wikipedia_en_top1m_maxi_2025-09.zim.torrent
# Project Gutenberg - Public domain books (72G)
https://download.kiwix.org/zim/gutenberg/gutenberg_en_all_2023-08.zim.torrent
# iFixit - Repair guides (3.3G)
https://download.kiwix.org/zim/ifixit/ifixit_en_all_2025-12.zim.torrent
# Stack Exchange
https://download.kiwix.org/zim/stack_exchange/superuser.com_en_all_2025-12.zim.torrent
https://download.kiwix.org/zim/stack_exchange/math.stackexchange.com_en_all_2025-12.zim.torrent
# LibreTexts - Open educational resources
https://download.kiwix.org/zim/libretexts/libretexts.org_en_bio_2025-01.zim.torrent
https://download.kiwix.org/zim/libretexts/libretexts.org_en_chem_2025-01.zim.torrent
https://download.kiwix.org/zim/libretexts/libretexts.org_en_eng_2025-01.zim.torrent
https://download.kiwix.org/zim/libretexts/libretexts.org_en_math_2025-01.zim.torrent
https://download.kiwix.org/zim/libretexts/libretexts.org_en_phys_2025-01.zim.torrent
https://download.kiwix.org/zim/libretexts/libretexts.org_en_human_2025-01.zim.torrent
# DevDocs - Programming documentation
https://download.kiwix.org/zim/devdocs/devdocs_en_bash_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_c_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_click_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_cmake_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_cpp_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_css_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_django-rest-framework_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_django_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_docker_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_duckdb_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_fish_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_gcc_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_git_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_go_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_godot_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_hammerspoon_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_homebrew_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_javascript_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_kubectl_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_kubernetes_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_latex_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_lua_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_markdown_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_nginx_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_nix_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_postgresql_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_python_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_redis_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_sqlite_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_typescript_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_werkzeug_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_zig_2026-01.zim.torrent

View file

@ -0,0 +1,85 @@
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: zim-watcher
namespace: kiwix
spec:
schedule: "0 * * * *" # Every hour
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
serviceAccountName: zim-watcher
containers:
- name: watcher
image: bitnami/kubectl:latest
command: ["/bin/bash", "-c"]
args:
- |
set -euo pipefail
# Get current ZIM files (among all downloads)
# This picks up ZIMs from both declarative list AND manually added torrents
current_zims=$(ls -1 /data/*.zim 2>/dev/null | sort | md5sum | cut -d' ' -f1 || echo "empty")
# Get stored hash from deployment annotation
annotation='kiwix\.blumeops/zim-hash'
stored_hash=$(kubectl get deployment kiwix -n kiwix \
-o jsonpath="{.metadata.annotations.$annotation}" 2>/dev/null || echo "")
echo "Current ZIMs hash: $current_zims"
echo "Stored hash: $stored_hash"
# Also list what ZIMs we found
echo "ZIM files found:"
ls -la /data/*.zim 2>/dev/null || echo " (none)"
if [[ "$current_zims" != "$stored_hash" && "$current_zims" != "empty" ]]; then
echo "ZIM files changed, restarting kiwix deployment..."
kubectl annotate deployment kiwix -n kiwix "kiwix.blumeops/zim-hash=$current_zims" --overwrite
kubectl rollout restart deployment/kiwix -n kiwix
echo "Restart triggered"
else
echo "No changes detected"
fi
volumeMounts:
- name: torrents
mountPath: /data
readOnly: true
restartPolicy: OnFailure
volumes:
- name: torrents
persistentVolumeClaim:
claimName: torrents-storage
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: zim-watcher
namespace: kiwix
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: zim-watcher
namespace: kiwix
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: zim-watcher
namespace: kiwix
subjects:
- kind: ServiceAccount
name: zim-watcher
namespace: kiwix
roleRef:
kind: Role
name: zim-watcher
apiGroup: rbac.authorization.k8s.io

View file

@ -0,0 +1,97 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kiwix
namespace: kiwix
annotations:
# Track ZIM file changes for restart detection
kiwix.blumeops/zim-hash: ""
spec:
replicas: 1
selector:
matchLabels:
app: kiwix
template:
metadata:
labels:
app: kiwix
spec:
containers:
# Main kiwix-serve container
- name: kiwix-serve
image: ghcr.io/kiwix/kiwix-serve:3.8.1
args:
- --port=80
- /data/*.zim # Serves ALL .zim files, regardless of how they were added
ports:
- containerPort: 80
name: http
volumeMounts:
- name: torrents
mountPath: /data
readOnly: true
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "1Gi"
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
# Sidecar: Syncs declarative ZIM torrents to transmission
- name: torrent-sync
image: lscr.io/linuxserver/transmission:latest # Has transmission-remote CLI
command: ["/bin/bash", "-c"]
args:
- |
echo "Starting ZIM torrent sync sidecar"
# Initial sync
/scripts/sync-zim-torrents.sh || echo "Initial sync failed, will retry"
# Periodic sync every 30 minutes
while true; do
sleep 1800
/scripts/sync-zim-torrents.sh || echo "Sync failed, will retry"
done
env:
- name: TRANSMISSION_HOST
value: "transmission.torrent.svc.cluster.local"
- name: TRANSMISSION_PORT
value: "9091"
- name: TORRENT_LIST
value: "/config/torrents.txt"
volumeMounts:
- name: zim-torrents-config
mountPath: /config/torrents.txt
subPath: torrents.txt
- name: sync-script
mountPath: /scripts
resources:
requests:
memory: "32Mi"
cpu: "10m"
limits:
memory: "64Mi"
volumes:
- name: torrents
persistentVolumeClaim:
claimName: torrents-storage
- name: zim-torrents-config
configMap:
name: kiwix-zim-torrents
- name: sync-script
configMap:
name: zim-torrent-sync-script
defaultMode: 493 # 0755 in decimal

View file

@ -0,0 +1,19 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kiwix
namespace: kiwix
spec:
ingressClassName: tailscale
rules:
- host: kiwix
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: kiwix
port:
number: 80

View file

@ -0,0 +1,12 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: kiwix
resources:
- pvc.yaml
- configmap-zim-torrents.yaml
- configmap-sync-script.yaml
- deployment.yaml
- service.yaml
- ingress-tailscale.yaml
- cronjob-zim-watcher.yaml

View file

@ -0,0 +1,14 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: torrents-storage
namespace: kiwix
spec:
accessModes:
- ReadWriteMany # Need write for the sync sidecar to work
storageClassName: ""
volumeName: torrents-smb-pv
resources:
requests:
storage: 1Ti

View file

@ -0,0 +1,13 @@
---
apiVersion: v1
kind: Service
metadata:
name: kiwix
namespace: kiwix
spec:
selector:
app: kiwix
ports:
- name: http
port: 80
targetPort: 80

View file

@ -0,0 +1,5 @@
---
# SMB CSI driver Helm values
# Minimal configuration - defaults are generally fine for single-node minikube
controller:
replicas: 1

View file

@ -0,0 +1,63 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: transmission
namespace: torrent
spec:
replicas: 1
selector:
matchLabels:
app: transmission
template:
metadata:
labels:
app: transmission
spec:
containers:
- name: transmission
image: lscr.io/linuxserver/transmission:latest
env:
- name: PUID
value: "1000"
- name: PGID
value: "1000"
- name: TZ
value: "America/Los_Angeles"
ports:
- containerPort: 9091
name: web
- containerPort: 51413
name: peer-tcp
- containerPort: 51413
protocol: UDP
name: peer-udp
volumeMounts:
- name: downloads
mountPath: /downloads
- name: config
mountPath: /config
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
livenessProbe:
httpGet:
path: /transmission/web/
port: 9091
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /transmission/web/
port: 9091
initialDelaySeconds: 10
periodSeconds: 10
volumes:
- name: downloads
persistentVolumeClaim:
claimName: torrents-storage
- name: config
emptyDir: {} # Config is ephemeral; torrents persist in SMB

View file

@ -0,0 +1,19 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: transmission
namespace: torrent
spec:
ingressClassName: tailscale
rules:
- host: torrent
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: transmission
port:
number: 9091

View file

@ -0,0 +1,11 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: torrent
resources:
- pv-smb.yaml
- pvc.yaml
- deployment.yaml
- service.yaml
- ingress-tailscale.yaml
# Note: secret-smb.yaml.tpl must be applied manually with credentials from 1Password

View file

@ -0,0 +1,29 @@
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: torrents-smb-pv
spec:
capacity:
storage: 1Ti
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
mountOptions:
- dir_mode=0777
- file_mode=0777
- uid=1000
- gid=1000
- noperm
- mfsymlinks
- cache=strict
- noserverino # Required to prevent data corruption
csi:
driver: smb.csi.k8s.io
volumeHandle: torrents-smb-pv
volumeAttributes:
source: //sifaka/torrents
nodeStageSecretRef:
name: smbcreds
namespace: torrent

View file

@ -0,0 +1,14 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: torrents-storage
namespace: torrent
spec:
accessModes:
- ReadWriteMany
storageClassName: ""
volumeName: torrents-smb-pv
resources:
requests:
storage: 1Ti

View file

@ -0,0 +1,14 @@
# Template - apply manually with credentials from 1Password
# kubectl --context=minikube-indri create secret generic smbcreds \
# --namespace torrent \
# --from-literal=username=$(op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/synology-smb-k8s/username") \
# --from-literal=password=$(op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/synology-smb-k8s/password")
apiVersion: v1
kind: Secret
metadata:
name: smbcreds
namespace: torrent
type: Opaque
stringData:
username: "{{ op://vg6xf6vvfmoh5hqjjhlhbeoaie/synology-smb-k8s/username }}"
password: "{{ op://vg6xf6vvfmoh5hqjjhlhbeoaie/synology-smb-k8s/password }}"

View file

@ -0,0 +1,13 @@
---
apiVersion: v1
kind: Service
metadata:
name: transmission
namespace: torrent
spec:
selector:
app: transmission
ports:
- name: web
port: 9091
targetPort: 9091