Author SHA1 Message Date
rock 8fa5053aef fix: allow paperless direct LLM access, bypass gateway JWT
- Add paperless namespace to llm-serving NetworkPolicy ingress
- Point paperless-ai CUSTOM_BASE_URL to reasoning-predictor directly
- Gateway JWT auth not yet available for service accounts
- paperless-ai-agent has llm:inference role in Authentik
2026-09-14 08:09:26 +09:00
rock 41e894f79e fix: aggressive 2m cleanup for completed jobs/pods, add PAPERLESS_USERNAME to paperless-ai
- stale-job-cleanup: run every 2m, delete completed/failed jobs+pods after 2m
- paperless-ai: add PAPERLESS_USERNAME=admin (required to find own user ID)
2026-09-14 07:56:05 +09:00
poimenandrock 86986013d7 fix: use CiliumNetworkPolicy for kube-apiserver egress (#49)
Standard K8s NetworkPolicy ipBlock rules don't work under Cilium for API server access. Use Cilium-native kube-apiserver entity instead.

Tested: runner pod can now reach K8s API server.
Required by: homelab-frontend PR #25 (Tekton CI).

---------

Co-authored-by: rock <[email protected]>
Reviewed-on: #49
Co-authored-by: poimen <[email protected]>
2026-09-13 14:04:35 +00:00
poimenandrock 4b5ecfcd49 fix: allow CI runner egress to K8s API server (#48)
CI runner needs kubectl access to create Tekton PipelineRuns for integration testing.

## Root Cause

The runner egress NetworkPolicy blocks `192.168.1.0/24` (LAN). The K8s API server runs on control-plane nodes in that subnet (`192.168.1.166:6443`). kubectl from inside the DinD container times out.

## Fix

Allow TCP port 6443 to `192.168.1.0/24` — scoped to control-plane API server only.

## Required By

homelab-frontend PR #25 (Tekton integration testing) — CI creates PipelineRuns via kubectl.

---------

Co-authored-by: rock <[email protected]>
Reviewed-on: #48
Co-authored-by: poimen <[email protected]>
2026-09-13 13:54:31 +00:00
4 changed files with 62 additions and 21 deletions
+14
View File
@@ -52,6 +52,20 @@ spec:
port: 8080
- protocol: TCP
port: 9000
# Allow from paperless namespace (paperless-ai auto-tagging)
# Bypasses gateway until service-account JWT token exchange is implemented.
# paperless-ai-agent has llm:inference role in Authentik.
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: paperless
ports:
- protocol: TCP
port: 8080
- protocol: TCP
port: 80
- protocol: TCP
port: 8000
# Allow intra-namespace (pod-to-pod within llm-serving)
- from:
- podSelector:
+1 -1
View File
@@ -39,7 +39,7 @@ spec:
- name: AI_PROVIDER
value: "custom"
- name: CUSTOM_BASE_URL
value: "http://api-gateway.api.svc.cluster.local:8080/v1"
value: "http://reasoning-predictor.llm-serving.svc.cluster.local:80/v1"
- name: CUSTOM_API_KEY
value: "not-required"
- name: CUSTOM_MODEL
@@ -1,12 +1,12 @@
# Cluster-wide cleanup of stale failed/completed Jobs and Pods.
# Runs daily at 04:00 UTC. Deletes:
# - Failed Jobs older than 24h (any namespace)
# - Completed Jobs older than 72h with no owning CronJob
# - Orphan pods in Error/Failed/Evicted state older than 1h
# Runs every 2 minutes. Deletes:
# - Failed Jobs older than 2m (any namespace)
# - Completed standalone Jobs older than 2m with no owning CronJob
# - Orphan pods in Error/Failed/Evicted/Completed state older than 2m
#
# CronJob-owned Jobs are managed by failedJobsHistoryLimit/successfulJobsHistoryLimit,
# but standalone Jobs (helm hooks, one-off runs, longhorn maintenance) have no TTL
# and linger forever.
# but standalone Jobs (helm hooks, one-off runs, CI TaskRuns) have no TTL
# and linger forever. Aggressive schedule keeps the cluster clean.
apiVersion: v1
kind: ServiceAccount
@@ -47,7 +47,7 @@ metadata:
labels:
app: stale-job-cleanup
spec:
schedule: "0 4 * * *"
schedule: "*/2 * * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
@@ -74,7 +74,9 @@ spec:
set -e
NOW=$(date +%s)
echo "=== Cleaning failed Jobs older than 24h ==="
TTL=120 # 2 minutes in seconds
echo "=== Cleaning failed Jobs older than 2m ==="
kubectl get jobs --all-namespaces -o json | \
jq -r '.items[] |
select(.status.conditions[]?.type == "Failed") |
@@ -82,15 +84,15 @@ spec:
"\(.metadata.namespace) \(.metadata.name) \(.status.startTime // .status.completionTime // .metadata.creationTimestamp)"' | \
while read -r NS NAME TS; do
JOB_EPOCH=$(date -d "$TS" +%s 2>/dev/null || echo 0)
AGE_H=$(( (NOW - JOB_EPOCH) / 3600 ))
if [ "$AGE_H" -ge 24 ]; then
echo "[delete] $NS/$NAME (failed ${AGE_H}h ago)"
AGE=$(( NOW - JOB_EPOCH ))
if [ "$AGE" -ge "$TTL" ]; then
echo "[delete] $NS/$NAME (failed ${AGE}s ago)"
kubectl delete job "$NAME" -n "$NS" --cascade=foreground 2>/dev/null || true
fi
done
echo ""
echo "=== Cleaning completed standalone Jobs older than 72h ==="
echo "=== Cleaning completed standalone Jobs older than 2m ==="
kubectl get jobs --all-namespaces -o json | \
jq -r '.items[] |
select(.status.succeeded >= 1) |
@@ -98,20 +100,20 @@ spec:
"\(.metadata.namespace) \(.metadata.name) \(.status.completionTime // .metadata.creationTimestamp)"' | \
while read -r NS NAME TS; do
JOB_EPOCH=$(date -d "$TS" +%s 2>/dev/null || echo 0)
AGE_H=$(( (NOW - JOB_EPOCH) / 3600 ))
if [ "$AGE_H" -ge 72 ]; then
echo "[delete] $NS/$NAME (completed ${AGE_H}h ago, no owner)"
AGE=$(( NOW - JOB_EPOCH ))
if [ "$AGE" -ge "$TTL" ]; then
echo "[delete] $NS/$NAME (completed ${AGE}s ago, no owner)"
kubectl delete job "$NAME" -n "$NS" --cascade=foreground 2>/dev/null || true
fi
done
echo ""
echo "=== Cleaning orphan Error/Failed/Evicted pods older than 1h ==="
# Evicted pods show as Failed with reason Evicted
echo "=== Cleaning orphan Error/Failed/Evicted/Completed pods older than 2m ==="
kubectl get pods --all-namespaces -o json | \
jq -r '.items[] |
select(
.status.phase == "Failed" or
.status.phase == "Succeeded" or
(.status.reason // "") == "Evicted" or
(.status.containerStatuses // [] | any(.state.terminated.reason == "Error"))
) |
@@ -119,9 +121,9 @@ spec:
"\(.metadata.namespace) \(.metadata.name) \(.metadata.creationTimestamp)"' | \
while read -r NS NAME TS; do
POD_EPOCH=$(date -d "$TS" +%s 2>/dev/null || echo 0)
AGE_H=$(( (NOW - POD_EPOCH) / 3600 ))
if [ "$AGE_H" -ge 1 ]; then
echo "[delete] $NS/$NAME (error/evicted ${AGE_H}h ago)"
AGE=$(( NOW - POD_EPOCH ))
if [ "$AGE" -ge "$TTL" ]; then
echo "[delete] $NS/$NAME (stale ${AGE}s ago)"
kubectl delete pod "$NAME" -n "$NS" --force 2>/dev/null || true
fi
done
@@ -0,0 +1,25 @@
# CiliumNetworkPolicy for kube-apiserver access.
#
# Standard K8s NetworkPolicy ipBlock rules don't work for the API server
# under Cilium — the except clause on 192.168.1.0/24 blocks the post-DNAT
# destination even when a separate rule re-allows a /32 or subnet.
#
# Cilium's native `kube-apiserver` entity resolves this correctly: it
# tracks the API server endpoints regardless of ClusterIP vs node-IP
# routing, so the policy stays valid across node changes and NAT paths.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: {{ .Release.Name }}-apiserver
namespace: {{ .Release.Namespace }}
spec:
endpointSelector:
matchLabels:
app: {{ .Release.Name }}
egress:
- toEntities:
- kube-apiserver
toPorts:
- ports:
- port: "6443"
protocol: TCP