The init container fetched a token once at pod start with a client_credentials grant against a web app. A web app has no such grant, so the fetch could not work; and a token fetched once is dead within the hour while the pod runs for weeks. llm-token-vendor runs beside the app. It signs a machine-key assertion for the paperless-llm service account, exchanges it for a token, and rewrites /app/data/llm_token.txt atomically at 80% of the token lifetime. The vendor image keeps using CUSTOM_API_KEY_FILE, so it needs no change and no OIDC support. The key is re-read on every mint, so a rotation lands without a restart: publish the new key, the next mint uses it, retire the old one. paperless-llm is a separate service account from poimen-llm, so revoking one caller leaves the other working.
126 lines
4.8 KiB
YAML
126 lines
4.8 KiB
YAML
# Secret paperless-ai-config managed via SOPS (argocd/secrets/paperless-ai-secrets.enc.yaml)
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: paperless-ai
|
|
namespace: paperless
|
|
labels:
|
|
app.kubernetes.io/name: paperless-ai
|
|
spec:
|
|
replicas: 1
|
|
# Recreate, not RollingUpdate: paperless-ai-data is a single RWO PVC. With
|
|
# RollingUpdate the new pod tries to mount the PVC while the old pod still
|
|
# holds it -> "Multi-Attach error" / stuck Init:0/1 forever, rollout wedged
|
|
# at "1 desired, 1 ready, 1 updated, 1 unavailable". Recreate terminates the
|
|
# old pod first so the PVC detaches before the new one starts. Same pattern
|
|
# as the Forgejo gitea Deployment.
|
|
strategy:
|
|
type: Recreate
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: paperless-ai
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: paperless-ai
|
|
spec:
|
|
tolerations:
|
|
- key: node-role.kubernetes.io/control-plane
|
|
operator: Exists
|
|
effect: NoSchedule
|
|
volumes:
|
|
- name: paperless-ai-data
|
|
persistentVolumeClaim:
|
|
claimName: paperless-ai-data
|
|
- name: paperless-ai-env-config
|
|
configMap:
|
|
name: paperless-ai-env
|
|
- name: llm-key
|
|
secret:
|
|
secretName: paperless-llm-key
|
|
defaultMode: 0400
|
|
# No init container. The token used to be fetched once at pod start with a
|
|
# client_credentials grant against a web app, which cannot work: a web app has
|
|
# no such grant, and even a token that was minted would be dead within the hour
|
|
# while this pod runs for weeks.
|
|
#
|
|
# llm-token-vendor runs beside the app instead. It signs a machine-key assertion,
|
|
# exchanges it for a token, and rewrites /app/data/llm_token.txt at 80% of the
|
|
# token lifetime. The vendor image keeps reading CUSTOM_API_KEY_FILE unchanged.
|
|
# The key is re-read on every mint, so a rotation lands without a restart.
|
|
containers:
|
|
- name: llm-token-vendor
|
|
image: forgejo.riotpiao.com/riotpiao-poimen/llm-token-vendor:0.1.0
|
|
imagePullPolicy: Always
|
|
args:
|
|
- --token-url=https://zitadel.riotpiao.com/oauth/v2/token
|
|
- --issuer=https://zitadel.riotpiao.com
|
|
- --user-id=397072668735570071
|
|
- --key-id=397072674272051351
|
|
- --key-file=/var/run/llm-key/key
|
|
- --scope=openid urn:zitadel:iam:org:project:id:396757796898471937:aud
|
|
- --out=/app/data/llm_token.txt
|
|
volumeMounts:
|
|
- {name: paperless-ai-data, mountPath: /app/data}
|
|
- {name: llm-key, mountPath: /var/run/llm-key, readOnly: true}
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
capabilities: {drop: ["ALL"]}
|
|
runAsNonRoot: true
|
|
runAsUser: 65532
|
|
seccompProfile: {type: RuntimeDefault}
|
|
resources:
|
|
requests: {cpu: 10m, memory: 32Mi}
|
|
limits: {cpu: 100m, memory: 64Mi}
|
|
- name: paperless-ai
|
|
image: clusterzx/paperless-ai:latest
|
|
env:
|
|
# Paperless-ngx connection
|
|
- name: PAPERLESS_API_URL
|
|
value: "http://paperless.paperless.svc.cluster.local:8000"
|
|
- name: PAPERLESS_API_TOKEN
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: paperless-ai-config
|
|
key: PAPERLESS_API_TOKEN
|
|
- name: PAPERLESS_USERNAME
|
|
value: "admin"
|
|
# LLM API — via public gateway with JWT auth
|
|
- name: AI_PROVIDER
|
|
value: "custom"
|
|
- name: CUSTOM_BASE_URL
|
|
value: "https://api.riotpiao.com/v1"
|
|
# Token will be read from file at runtime by the application
|
|
# The init container fetches it and stores in /app/data/llm_token.txt
|
|
- name: CUSTOM_API_KEY_FILE
|
|
value: "/app/data/llm_token.txt"
|
|
- name: CUSTOM_MODEL
|
|
value: "qwen2.5:3b-instruct"
|
|
# Behavior - scan for new documents and tag them
|
|
- name: SCAN_INTERVAL
|
|
value: "60"
|
|
- name: PROCESS_PREDEFINED_DOCUMENTS
|
|
value: "no"
|
|
- name: ADD_AI_TAG
|
|
value: "yes"
|
|
- name: AI_TAG_NAME
|
|
value: "ai-processed"
|
|
- name: USE_PROMPT_TAGS
|
|
value: "yes"
|
|
- name: ADD_AI_DESCRIPTIONS
|
|
value: "yes"
|
|
volumeMounts:
|
|
- name: paperless-ai-data
|
|
mountPath: /app/data
|
|
- name: paperless-ai-env-config
|
|
mountPath: /app/data/.env
|
|
subPath: paperless-ai-config.env
|
|
resources:
|
|
requests:
|
|
cpu: 100m
|
|
memory: 512Mi
|
|
limits:
|
|
cpu: "1"
|
|
memory: 2Gi
|