diff --git a/shortsmith/argocd-app.yaml b/shortsmith/argocd-app.yaml new file mode 100644 index 0000000..8240a03 --- /dev/null +++ b/shortsmith/argocd-app.yaml @@ -0,0 +1,23 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: shortsmith + namespace: argocd + annotations: + notifications.argoproj.io/subscribe.on-sync-failed.telegram: "5138407666" + notifications.argoproj.io/subscribe.on-health-degraded.telegram: "5138407666" +spec: + project: default + source: + repoURL: https://git.chemavx.xyz/chemavx/k8s-manifests + targetRevision: HEAD + path: shortsmith + destination: + server: https://kubernetes.default.svc + namespace: shortsmith + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/shortsmith/deployment.yaml b/shortsmith/deployment.yaml new file mode 100644 index 0000000..25dca08 --- /dev/null +++ b/shortsmith/deployment.yaml @@ -0,0 +1,143 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: shortsmith + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + annotations: + # Never let ArgoCD prune this PVC: it holds the job store and the rendered videos. + argocd.argoproj.io/sync-options: Prune=false + name: shortsmith-data + namespace: shortsmith +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: shortsmith + namespace: shortsmith + labels: + app: shortsmith +spec: + replicas: 1 + # Single replica on a RWO local-path volume holding a SQLite job store in WAL mode: + # Recreate keeps two pods from opening /data/shortsmith.db during a roll. The queue + # is per-process anyway, so a second replica would not share work, only the file. + strategy: + type: Recreate + selector: + matchLabels: + app: shortsmith + template: + metadata: + labels: + app: shortsmith + spec: + containers: + - name: shortsmith + image: git.chemavx.xyz/chemavx/shortsmith:2dc2a8bb + imagePullPolicy: Always + ports: + - name: http + containerPort: 8080 + env: + - name: SHORTSMITH_DATA_DIR + value: "/data" + volumeMounts: + - name: data + mountPath: /data + # Measured on chemavx-k8 under `systemd-run --scope -p CPUQuota=300%`, + # sampling the cgroup's memory.stat anon every 20 ms — the figure that + # cannot be reclaimed. Rendering is this service's normal mode, not a + # burst, so the request comes from the peak and not from the resting 22 MB. + # + # at rest (imports + FastAPI app) 22 MB + # frames, 3 workers, 1260 of them 147 MB + # audio synthesis and WAV 71 MB + # encode, -threads 3 480 MB <- the whole cost + # full render of examples/jal1628.json 522 MB, 32 s + # the same at the spec's 180 s ceiling 511 MB, 138 s + # + # The peak is the encode's and does not grow with duration: at 5400 frames + # anon is unchanged and only the page cache grows (64 -> 275 MB), which is + # reclaimable. That is why these are sized against anon and not against + # memory.current, which reaches 808 MB at the ceiling. + # + # 512Mi does survive the reference render, but only by evicting page cache + # to sit exactly on the cap with no margin. 640Mi covers the measured peak + # with ~20% headroom; the 1Gi limit lets the page cache stay cached instead + # of being re-read during the encode. + # + # The CPU limit is what sizes the worker pool, not the request — the code + # reads cpu.max, since cpu_count() reports the node's 16. At limits.cpu 3 + # that is 3 workers, verified in a real cgroup (300%->3, 200%->2, 100%->1). + # 3 CPUs render in 32 s against 48 s at 2, for 22 MB less, so the third is + # worth more than the extra worker costs. + # + # Consequence, deliberate: under sustained contention the pod runs 3 workers + # on the 1 CPU it is guaranteed and a render takes about 3x longer. That is + # benign here — POST /render returns a job id, the queue runs at concurrency + # 1 and the caller polls, so a slow render is a later `done`, never a + # timeout. Do not "fix" it by sizing the pool from the request: that would + # trade an occasional slowdown for a permanent 3x on every render, including + # the ones on an idle node, which is the common case. + resources: + requests: + cpu: "1" + memory: "640Mi" + limits: + cpu: "3" + memory: "1Gi" + # The render runs off the event loop (asyncio.to_thread, and the frame + # workers are separate processes), so /healthz answers while a job is in + # flight. A probe timing out means the process is actually wedged. + readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 15 + periodSeconds: 30 + failureThreshold: 3 + volumes: + - name: data + persistentVolumeClaim: + claimName: shortsmith-data + imagePullSecrets: + - name: gitea-registry-infisical + +--- +apiVersion: v1 +kind: Service +metadata: + # Must end in -svc. researchowl hit this: a Service named `searxng` collided with + # the SEARXNG_* env vars k8s injects into every pod in the namespace and returned + # 403s until it was renamed. + name: shortsmith-svc + namespace: shortsmith +spec: + selector: + app: shortsmith + ports: + - name: http + port: 8080 + targetPort: http + +# No Ingress on purpose: this is an internal API with no authentication, reached at +# shortsmith-svc.shortsmith.svc.cluster.local:8080 from inside the cluster. Ollama was +# exposed without auth once and had to be retrofitted with Authentik forward-auth. diff --git a/shortsmith/infisical-gitea-registry.yaml b/shortsmith/infisical-gitea-registry.yaml new file mode 100644 index 0000000..b3c216f --- /dev/null +++ b/shortsmith/infisical-gitea-registry.yaml @@ -0,0 +1,27 @@ +apiVersion: secrets.infisical.com/v1beta1 +kind: InfisicalStaticSecret +metadata: + name: gitea-registry + namespace: shortsmith +spec: + # References the SHARED auth layer in infisical-operator (cross-namespace). + infisicalAuthRef: + name: infisical-auth + namespace: infisical-operator + sources: + - projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab" + environmentSlug: prod + secretPath: /registry + syncOptions: + refreshInterval: 60s + targets: + - kind: Secret + name: gitea-registry-infisical + namespace: shortsmith + creationPolicy: Owner + secretType: kubernetes.io/dockerconfigjson + template: + engineVersion: v1 + data: + # Reconstruct the docker pull-secret blob from the two discrete /registry fields. + .dockerconfigjson: '{"auths":{"git.chemavx.xyz":{"username":"{{ .GITEA_REGISTRY_USERNAME.Value }}","password":"{{ .GITEA_REGISTRY_TOKEN.Value }}","auth":"{{ printf "%s:%s" .GITEA_REGISTRY_USERNAME.Value .GITEA_REGISTRY_TOKEN.Value | encodeBase64 }}"}}}'