Compare commits
70
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5589bcd56a | ||
|
|
fd7b714f09 | ||
|
|
6291dd5afb | ||
|
|
648388554d | ||
|
|
6ce46b9ad5 | ||
|
|
d3a059a6b4 | ||
|
|
f015e4577b | ||
|
|
1877f94bf6 | ||
|
|
c5ff86d6dc | ||
|
|
1bf611739b | ||
|
|
9fbfce7963 | ||
|
|
ac1849d2a9 | ||
|
|
4eab8271c7 | ||
|
|
dd491f6f8b | ||
|
|
5b041df884 | ||
|
|
3ea057d83e | ||
|
|
43c0e1faa2 | ||
|
|
3a244577e4 | ||
|
|
7a0d09cbe0 | ||
|
|
cb52356d13 | ||
|
|
d0cbfac7a4 | ||
|
|
930374a3b8 | ||
|
|
b10d1c3a25 | ||
|
|
4c54674dff | ||
|
|
05ff12c117 | ||
|
|
56b1c96fcf | ||
|
|
26a959215e | ||
|
|
193b040de6 | ||
|
|
12778d5576 | ||
|
|
cd6c620619 | ||
|
|
0a87302e19 | ||
|
|
c64b68a36b | ||
|
|
4146a048c9 | ||
|
|
f0fa1dbd27 | ||
|
|
2b1c4b1df4 | ||
|
|
479318c532 | ||
|
|
ff216429b9 | ||
|
|
7441aaf9c3 | ||
|
|
edd739198d | ||
|
|
20f8aac95d | ||
|
|
dbd3dc7b3d | ||
|
|
bebe8dc31b | ||
|
|
4c0d30ce30 | ||
|
|
a17ceedcd8 | ||
|
|
9a779ccaf4 | ||
|
|
20d0517f79 | ||
|
|
c8ea7b9190 | ||
|
|
d3e2215b5c | ||
|
|
c00b2d1b53 | ||
|
|
db6bf742da | ||
|
|
f6298086f2 | ||
|
|
fbc4e55718 | ||
|
|
06c35fb338 | ||
|
|
09fa9c6145 | ||
|
|
bd99208754 | ||
|
|
58605e1b5c | ||
|
|
40fcbd036c | ||
|
|
69b5fc371d | ||
|
|
582524f921 | ||
|
|
828e3fb287 | ||
|
|
1d5c18d62c | ||
|
|
bc1a6d8689 | ||
|
|
e4485412b0 | ||
|
|
2022595426 | ||
|
|
f67aaa41d0 | ||
|
|
69e8cfd6d1 | ||
|
|
db2fc9afc7 | ||
|
|
2b74b58ea6 | ||
|
|
27dbfb1bd7 | ||
|
|
4e67fd907a |
@@ -156,21 +156,43 @@ jobs:
|
|||||||
- name: Check for Secrets in Code
|
- name: Check for Secrets in Code
|
||||||
run: |
|
run: |
|
||||||
echo "=== Scanning for hardcoded secrets ==="
|
echo "=== Scanning for hardcoded secrets ==="
|
||||||
SECRETS_FOUND=0
|
# BLOCKING. This step used to only count findings and then exit 0, so a
|
||||||
|
# plaintext deploy key rode through it into a public remote. Two failure
|
||||||
|
# modes fixed: it now fails the build, and it matches key material by
|
||||||
|
# PEM header rather than only `private_key:`-style YAML field names.
|
||||||
|
# Findings are captured into variables and tested for emptiness rather than
|
||||||
|
# branching on grep's exit status: implementations disagree on the rc of a
|
||||||
|
# `-v` filter fed empty input, and a wrong rc here fails open.
|
||||||
|
# NOTE: --include must precede `--`; after `--` grep treats it as a filename
|
||||||
|
# and silently scans nothing.
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
for pattern in "password:" "secret:" "token:" "api_key:" "apikey:" "private_key:" "privatekey:"; do
|
# Any private key block is fatal, regardless of the field name carrying it.
|
||||||
if grep -r "$pattern" k8s/ --include="*.yaml" --include="*.yml" | grep -v "^Binary"; then
|
KEYS=$(grep -rIE --include="*.yaml" --include="*.yml" \
|
||||||
echo "⚠️ Found potential secret pattern: $pattern"
|
-- "-----BEGIN ([A-Z]+ )?PRIVATE KEY-----" k8s/ \
|
||||||
SECRETS_FOUND=$((SECRETS_FOUND + 1))
|
| grep -v "\.enc\.yaml" || true)
|
||||||
|
if [ -n "$KEYS" ]; then
|
||||||
|
echo "❌ Unencrypted private key material found:"
|
||||||
|
echo "$KEYS"
|
||||||
|
FAILED=1
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
|
|
||||||
if [ $SECRETS_FOUND -gt 0 ]; then
|
# Plaintext values in secret-ish YAML fields. SOPS output is ENC[...],
|
||||||
echo "⚠️ Warning: Found $SECRETS_FOUND potential secrets"
|
# so encrypted files never trip this.
|
||||||
echo "Secrets should be encrypted with SOPS or stored in ArgoCD Sealed Secrets"
|
VALS=$(grep -rInE --include="*.yaml" --include="*.yml" \
|
||||||
else
|
-- "^[[:space:]]*(password|token|apiKey|api_key|sshPrivateKey|client_secret):[[:space:]]*[\"']?[^\"'[:space:]{\$]{8,}" k8s/ \
|
||||||
|
| grep -v "ENC\[" | grep -v "\.enc\.yaml" || true)
|
||||||
|
if [ -n "$VALS" ]; then
|
||||||
|
echo "❌ Plaintext secret value found:"
|
||||||
|
echo "$VALS"
|
||||||
|
FAILED=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$FAILED" -ne 0 ]; then
|
||||||
|
echo "Encrypt with SOPS (see .sops.yaml) — *.enc.yaml files are exempt."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
echo "✓ No hardcoded secrets found"
|
echo "✓ No hardcoded secrets found"
|
||||||
fi
|
|
||||||
|
|
||||||
# === Check K8s Security Best Practices ===
|
# === Check K8s Security Best Practices ===
|
||||||
- name: Check K8s Security Best Practices
|
- name: Check K8s Security Best Practices
|
||||||
|
|||||||
+11
-2
@@ -50,10 +50,19 @@ terraform/*.tfstate.*
|
|||||||
terraform.tfvars.local
|
terraform.tfvars.local
|
||||||
skills-lock.json
|
skills-lock.json
|
||||||
secrets-plaintext.yaml
|
secrets-plaintext.yaml
|
||||||
skills-lock.json
|
|
||||||
|
# Saved plan files — binary, environment-specific, may embed resource attributes
|
||||||
|
terraform/tfplan
|
||||||
|
terraform/tfplan-*
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
docs/
|
docs/
|
||||||
|
|
||||||
k8s/argocd/seed-repo-secret.yamlbootstrap-argocd.log
|
bootstrap-argocd.log
|
||||||
|
|
||||||
|
# Any plaintext (non-SOPS) secret manifest. Encrypted ones are *.enc.yaml and
|
||||||
|
# ARE committed — see .sops.yaml. A missing newline once merged two patterns on
|
||||||
|
# one line here, which is how a plaintext deploy key reached a public remote.
|
||||||
|
k8s/**/*-secret.yaml
|
||||||
|
!k8s/**/*.enc.yaml
|
||||||
|
|||||||
+4
-2
@@ -1,3 +1,5 @@
|
|||||||
creation_rules:
|
creation_rules:
|
||||||
- path_regex: k8s/.*secrets.*\.ya?ml
|
# `secrets?` — singular too. A `seed-repo-secret.yaml` once slipped this regex
|
||||||
age: age1smu533f803gmd0jq60s2zaj9zlznajy0ca6rtewd4r37mr2hs3uqsrldfh
|
# and was committed in plaintext to a public remote.
|
||||||
|
- path_regex: k8s/.*secrets?.*\.ya?ml
|
||||||
|
age: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# ── Node IPs (3-CP HA topology) ───────────────────────────────────────────────
|
# ── Node IPs (3-CP HA topology) ───────────────────────────────────────────────
|
||||||
CP1_IP := 192.168.1.166 # talos-cp-1
|
CP1_IP := 192.168.1.166 # talos-cp-1
|
||||||
CP2_IP := 192.168.1.213 # talos-cp-2 (storage: 3 disks)
|
CP2_IP := 192.168.1.214 # talos-cp-2 (storage: 3 disks)
|
||||||
CP3_IP := 192.168.1.162 # talos-cp-3
|
CP3_IP := 192.168.1.162 # talos-cp-3
|
||||||
CP_VIP := 192.168.1.166 # controlplane VIP (currently .166)
|
CP_VIP := 192.168.1.166 # controlplane VIP (currently .166)
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ W_CONFIG = cluster-config/worker-$(N).yaml
|
|||||||
# ── Help ──────────────────────────────────────────────────────────────────────
|
# ── Help ──────────────────────────────────────────────────────────────────────
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
help:
|
help:
|
||||||
@echo "Homelab cluster (3-CP HA: .166/.213/.163) — available targets"
|
@echo "Homelab cluster (3-CP HA: .166/.214/.162) — available targets"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " Status & Services"
|
@echo " Status & Services"
|
||||||
@echo " nodes kubectl get nodes"
|
@echo " nodes kubectl get nodes"
|
||||||
|
|||||||
+5
-10
@@ -12,16 +12,15 @@
|
|||||||
# - Talos cluster up; kubectl context points at it
|
# - Talos cluster up; kubectl context points at it
|
||||||
# - helm 3, kubectl
|
# - helm 3, kubectl
|
||||||
# - SOPS age key at $SOPS_KEY (for the ArgoCD SOPS CMP plugin)
|
# - SOPS age key at $SOPS_KEY (for the ArgoCD SOPS CMP plugin)
|
||||||
# - GitHub read-only deploy key private half at $DEPLOY_KEY (public half added
|
#
|
||||||
# to the GitHub repo's Deploy keys)
|
# The GitHub seed repo is public, so it is cloned anonymously over HTTPS — no
|
||||||
|
# deploy key, no repository Secret, one less thing to bootstrap before ArgoCD.
|
||||||
#
|
#
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
BOOT="$SCRIPT_DIR/k8s/bootstrap"
|
BOOT="$SCRIPT_DIR/k8s/bootstrap"
|
||||||
SOPS_KEY="${SOPS_KEY:-$HOME/.sops/key.txt}"
|
SOPS_KEY="${SOPS_KEY:-$HOME/.sops/key.txt}"
|
||||||
DEPLOY_KEY="${DEPLOY_KEY:-$HOME/.ssh/argocd_seed}"
|
|
||||||
GITHUB_SSH="[email protected]:Riotpiaole/riotpiao.homelab.com.git"
|
|
||||||
|
|
||||||
log() { echo "[$(date +%H:%M:%S)] $*"; }
|
log() { echo "[$(date +%H:%M:%S)] $*"; }
|
||||||
die() { echo "ERROR: $*" >&2; exit 1; }
|
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||||
@@ -39,7 +38,6 @@ preflight() {
|
|||||||
kubectl cluster-info >/dev/null || die "kubectl not configured / cluster unreachable"
|
kubectl cluster-info >/dev/null || die "kubectl not configured / cluster unreachable"
|
||||||
command -v helm >/dev/null || die "helm 3 not found"
|
command -v helm >/dev/null || die "helm 3 not found"
|
||||||
[[ -f "$SOPS_KEY" ]] || die "SOPS age key missing at $SOPS_KEY"
|
[[ -f "$SOPS_KEY" ]] || die "SOPS age key missing at $SOPS_KEY"
|
||||||
[[ -f "$DEPLOY_KEY" ]] || die "GitHub deploy key missing at $DEPLOY_KEY (see phase4-argocd/seed-repo-secret.example.yaml)"
|
|
||||||
log "✅ preflight ok"
|
log "✅ preflight ok"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,12 +192,9 @@ p3_forgejo() {
|
|||||||
p4_argocd() {
|
p4_argocd() {
|
||||||
phase "PHASE 4: ArgoCD (seeded from GitHub)"
|
phase "PHASE 4: ArgoCD (seeded from GitHub)"
|
||||||
|
|
||||||
# Always ensure namespace + repository secret (idempotent)
|
# Always ensure namespace (idempotent). The seed repo is public — ArgoCD clones
|
||||||
|
# it anonymously over HTTPS, so there is no repository Secret to create.
|
||||||
kubectl create ns argocd --dry-run=client -o yaml | kubectl apply -f -
|
kubectl create ns argocd --dry-run=client -o yaml | kubectl apply -f -
|
||||||
kubectl -n argocd create secret generic seed-github-repo \
|
|
||||||
--from-literal=type=git --from-literal=url="$GITHUB_SSH" \
|
|
||||||
--from-file=sshPrivateKey="$DEPLOY_KEY" --dry-run=client -o yaml | kubectl apply -f -
|
|
||||||
kubectl -n argocd label secret seed-github-repo argocd.argoproj.io/secret-type=repository --overwrite 2>/dev/null || true
|
|
||||||
|
|
||||||
# Decrypt and apply any encrypted secrets from bootstrap dir (local SOPS)
|
# Decrypt and apply any encrypted secrets from bootstrap dir (local SOPS)
|
||||||
if command -v sops &> /dev/null; then
|
if command -v sops &> /dev/null; then
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
data:
|
||||||
|
settings.json: |
|
||||||
|
{
|
||||||
|
"defaultProvider": "homelab-ornith",
|
||||||
|
"defaultModel": "ornith:35b",
|
||||||
|
"defaultThinkingLevel": "medium",
|
||||||
|
"theme": "light",
|
||||||
|
"compaction": {
|
||||||
|
"enabled": true,
|
||||||
|
"reserveTokens": 16000,
|
||||||
|
"keepRecentTokens": 6000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: pi-config
|
||||||
|
namespace: agent-pod
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Exposes agent-hub at api.riotpiao.com/console (WebSocket) and /run
|
||||||
|
# (trigger a new session) -- both are routes on the same hub.js service.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: console
|
||||||
|
namespace: agent-pod
|
||||||
|
annotations:
|
||||||
|
konghq.com/strip-path: "false"
|
||||||
|
spec:
|
||||||
|
ingressClassName: kong
|
||||||
|
rules:
|
||||||
|
- host: api.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /console
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: agent-hub
|
||||||
|
port:
|
||||||
|
number: 9090
|
||||||
|
- path: /run
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: agent-hub
|
||||||
|
port:
|
||||||
|
number: 9090
|
||||||
|
- path: /sessions
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: agent-hub
|
||||||
|
port:
|
||||||
|
number: 9090
|
||||||
@@ -0,0 +1,966 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: coordinator-src
|
||||||
|
namespace: agent-pod
|
||||||
|
data:
|
||||||
|
coordinator.js: |
|
||||||
|
#!/usr/bin/env node
|
||||||
|
// coordinator: local CLI that drives a multi-phase, multi-task pipeline of
|
||||||
|
// planner/investigator/implementer/judge stages, same state machine as
|
||||||
|
// hub.js's old /pipeline handler. Every interactive stage runs through the
|
||||||
|
// patched agent-manager fork's `spawn` subcommand, so it is a real tracked
|
||||||
|
// session (tmux pane on agent-manager's private socket + a state.db row)
|
||||||
|
// from the moment it exists -- attachable and visible in agent-manager's
|
||||||
|
// own TUI the whole time it runs.
|
||||||
|
//
|
||||||
|
// Per repo, each role (planner/investigator/implementer/judge) is ONE
|
||||||
|
// persistent agent-manager session, not a fresh spawn per task: the first
|
||||||
|
// task to need a role spawns it, every later task for that role reuses the
|
||||||
|
// same tmux pane via `tmux send-keys` (see runOnPool) -- the same nudge
|
||||||
|
// mechanism that used to only fire on a stall now doubles as "give this
|
||||||
|
// agent its next task." Every role reads everything it needs fresh off disk
|
||||||
|
// each call, so every pane gets a `/new` before every reuse instead of
|
||||||
|
// accumulating history that degrades and eventually errors out task after
|
||||||
|
// task -- same pane, same agent-manager session, zero memory of the last
|
||||||
|
// task it handled.
|
||||||
|
// Because only one implementer/judge/etc. exists per repo, tasks within a
|
||||||
|
// phase run strictly sequentially against the pool -- no per-task worktree,
|
||||||
|
// no per-task branch, no merge-back step; every task commits straight onto
|
||||||
|
// the phase branch in the repo's one shared clone.
|
||||||
|
//
|
||||||
|
// The unit of concurrency is now the REPO, not the task: runCoordinator
|
||||||
|
// takes a list of repos and runs up to REPO_CONCURRENCY of them at once,
|
||||||
|
// each with its own clone (under WORK_DIR/<repoId>) and its own 4-agent
|
||||||
|
// pool. The coordinator never kills a role's session; it rests at an idle
|
||||||
|
// prompt between tasks, and agent-manager's session list becomes the audit
|
||||||
|
// trail of everything every repo's pipeline ran. Completion is signaled by
|
||||||
|
// sentinel files under the repo's clone (unchanged convention), waited on
|
||||||
|
// with fs.watch instead of polling.
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
const { spawn } = require("node:child_process");
|
||||||
|
|
||||||
|
const WORK_DIR = process.env.HUB_WORK_DIR || path.join(require("node:os").tmpdir(), "agent-harness-work");
|
||||||
|
|
||||||
|
// Never rely on a bare `pi`/`agent-manager` on $PATH -- see PI_BIN's own
|
||||||
|
// comment below; the same collision risk applies to any CLI name. Always
|
||||||
|
// invoke explicit pinned paths.
|
||||||
|
const PI_BIN =
|
||||||
|
process.env.PI_BIN ||
|
||||||
|
path.join(__dirname, "..", ".pi-cli", "node_modules", "@earendil-works", "pi-coding-agent", "dist", "cli.js");
|
||||||
|
|
||||||
|
const AGENT_MANAGER_BIN = process.env.AGENT_MANAGER_BIN || path.join(__dirname, "..", ".bin", "agent-manager-fork");
|
||||||
|
|
||||||
|
// agent-manager's own session-state DB -- used to detect a session that has
|
||||||
|
// actually died (process crashed/exited, status flips to "errored"/"dead")
|
||||||
|
// instead of one that's merely slow. Read-only introspection plus the one
|
||||||
|
// UPDATE in killDeadSession below, same class of operation as the tmux
|
||||||
|
// nudges already done directly against agent-manager's internals.
|
||||||
|
const AGENT_MANAGER_DB =
|
||||||
|
process.env.AGENT_MANAGER_DB || path.join(require("node:os").homedir(), ".config", "agent-manager", "state.db");
|
||||||
|
|
||||||
|
// Empty means "let pi fall back to ~/.pi/agent/settings.json's default"
|
||||||
|
// (currently anthropic/claude-sonnet-4-5, real paid usage). Set both to
|
||||||
|
// route every stage -- headless (spawnPi) and interactive (runOnPool) --
|
||||||
|
// at the homelab model instead, e.g. AGENT_PROVIDER=homelab-ornith
|
||||||
|
// AGENT_MODEL=ornith:35b.
|
||||||
|
const AGENT_PROVIDER = process.env.AGENT_PROVIDER || "";
|
||||||
|
const AGENT_MODEL = process.env.AGENT_MODEL || "";
|
||||||
|
|
||||||
|
// judge can run a different model than the rest of the chain, e.g.
|
||||||
|
// homelab-reasoning instead of homelab-ornith now that verifier/PRM is
|
||||||
|
// retired. Falls back to AGENT_PROVIDER/AGENT_MODEL when unset, so a run
|
||||||
|
// that doesn't care keeps one uniform model everywhere.
|
||||||
|
const JUDGE_PROVIDER = process.env.JUDGE_PROVIDER || AGENT_PROVIDER;
|
||||||
|
const JUDGE_MODEL = process.env.JUDGE_MODEL || AGENT_MODEL;
|
||||||
|
|
||||||
|
function providerModelFor(role) {
|
||||||
|
return role === "judge" ? { provider: JUDGE_PROVIDER, model: JUDGE_MODEL } : { provider: AGENT_PROVIDER, model: AGENT_MODEL };
|
||||||
|
}
|
||||||
|
|
||||||
|
// agent-manager's private tmux server and session-naming scheme
|
||||||
|
// (internal/tmux/tmux.go: defaultSocket = "agentmgr", sessionName(id) =
|
||||||
|
// "am_"+id) -- stable, documented internals of the fork, used here only
|
||||||
|
// for read-only introspection (pane capture) and role nudges, exactly the
|
||||||
|
// class of operation hub.js already ran directly against its own sessions
|
||||||
|
// rather than asking a model to do it.
|
||||||
|
const AM_SOCKET = "agentmgr";
|
||||||
|
function amSessionName(id) {
|
||||||
|
return `am_${id}`;
|
||||||
|
}
|
||||||
|
function runAmTmux(args) {
|
||||||
|
return runCmd("tmux", ["-L", AM_SOCKET, ...args]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROLE_SKILLS = new Set(["planner", "investigator", "info-collector", "implementer", "judge", "resolver"]);
|
||||||
|
|
||||||
|
// Every role reads everything it needs fresh off disk each call -- PLAN.md,
|
||||||
|
// the task spec, judge's verdict file, `git diff` against baseBranch --
|
||||||
|
// nothing depends on remembering earlier tasks. Left to accumulate, a
|
||||||
|
// pooled session's conversation grows without bound across every task in a
|
||||||
|
// repo and both correctness and reliability degrade hard once it does
|
||||||
|
// (observed: a planner session at ~1.5M cumulative tokens started erroring
|
||||||
|
// out every call, an investigator session that far gone started narrating a
|
||||||
|
// different codebase entirely). So every role gets reset to a clean
|
||||||
|
// conversation before every reuse instead of just being nudged with the
|
||||||
|
// next prompt -- same pane, same agent-manager session (still
|
||||||
|
// visible/attachable), zero history carried between tasks.
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
const HARD_RULES =
|
||||||
|
"Read and follow ~/.pi/agent/skills/karpathy-guidelines/SKILL.md and " +
|
||||||
|
"~/.pi/agent/skills/caveman/SKILL.md as hard rules for this entire task, before anything else. ";
|
||||||
|
|
||||||
|
// judge (routed to homelab-reasoning) has been observed narrating an
|
||||||
|
// entire review in prose -- "I should run git diff, then check X..." --
|
||||||
|
// and then writing a verdict based on that narration without ever calling
|
||||||
|
// a real tool. Live example: a phase-judge call produced a page of
|
||||||
|
// "I would check..." reasoning, declared VERDICT: PASS, and showed the
|
||||||
|
// touch command as a fenced code block IN ITS OWN TEXT rather than
|
||||||
|
// executing it. Coordinator just timed out waiting on a sentinel that was
|
||||||
|
// never going to appear, since nothing was ever actually run. Spelled out
|
||||||
|
// explicitly since "use the judge skill" alone apparently isn't enough to
|
||||||
|
// rule this out.
|
||||||
|
const REQUIRE_REAL_TOOL_CALLS =
|
||||||
|
"Do not narrate what you would check -- actually run the commands via a real tool call and read their real " +
|
||||||
|
"output before writing anything. A verdict based on describing checks instead of executing them is invalid. " +
|
||||||
|
"Writing the verdict file and touching the sentinel are themselves tool calls you must execute, not text to " +
|
||||||
|
"display in your response. ";
|
||||||
|
|
||||||
|
function parseVerdictLine(text, label) {
|
||||||
|
if (!text) return null;
|
||||||
|
const re = new RegExp(`${label}:\\s*(\\w+)`, "i");
|
||||||
|
const m = text.match(re);
|
||||||
|
return m ? m[1].toUpperCase() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCmd(bin, args, cwd) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const child = spawn(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
||||||
|
let out = "";
|
||||||
|
child.stdout.on("data", (c) => (out += c));
|
||||||
|
child.stderr.on("data", (c) => (out += c));
|
||||||
|
child.on("close", (code) => resolve({ code, out: out.trim() }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runGit(cwd, args) {
|
||||||
|
return runCmd("git", args, cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A stage saying "commit" in its prompt is a request, not a guarantee -- seen
|
||||||
|
// in practice: a stage writes a real file and simply never runs `git add`/
|
||||||
|
// `git commit`, leaving it untracked and invisible to every later `git diff`.
|
||||||
|
// Sweep and commit anything left dirty after every stage, deterministically.
|
||||||
|
async function commitPending(cwd, message) {
|
||||||
|
await runGit(cwd, ["add", "-A"]);
|
||||||
|
const status = await runGit(cwd, ["status", "--porcelain"]);
|
||||||
|
if (!status.out) return { committed: false };
|
||||||
|
const commit = await runGit(cwd, ["commit", "-m", message]);
|
||||||
|
return { committed: commit.code === 0, error: commit.code !== 0 ? commit.out : undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headless one-shot pi call (`pi -p --mode json <prompt>`), used only for
|
||||||
|
// quick diagnostic/mechanical calls that don't need to be a watchable
|
||||||
|
// session: resolver's crash/stall diagnosis, and the initial clone. Kept
|
||||||
|
// exactly as before -- only the interactive per-role stages (runOnPool,
|
||||||
|
// below) go through agent-manager.
|
||||||
|
//
|
||||||
|
// Bounded by SPAWN_PI_TIMEOUT_MS -- unlike runOnPool's pooled sessions
|
||||||
|
// (which now have status polling to catch a dead session fast, see
|
||||||
|
// waitForSentinel/killDeadSession), this is a raw child_process with no
|
||||||
|
// equivalent escape hatch. Observed live: a resolver call shared the
|
||||||
|
// default backend with a concurrently-busy repo's implementer and sat for
|
||||||
|
// 6+ minutes producing nothing -- with no timeout here, that blocks the
|
||||||
|
// entire calling repo's pipeline forever, since askResolver is always
|
||||||
|
// awaited before the next stage can run.
|
||||||
|
const SPAWN_PI_TIMEOUT_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
function spawnPi({ agent, prompt, cwd }) {
|
||||||
|
const finalPrompt = ROLE_SKILLS.has(agent) ? `/skill:${agent} ${HARD_RULES}${prompt}` : prompt;
|
||||||
|
const args = ["-p", "--mode", "json"];
|
||||||
|
if (AGENT_PROVIDER) args.push("--provider", AGENT_PROVIDER);
|
||||||
|
if (AGENT_MODEL) args.push("--model", AGENT_MODEL);
|
||||||
|
args.push(finalPrompt);
|
||||||
|
const child = spawn(PI_BIN, args, { stdio: ["ignore", "pipe", "pipe"], cwd });
|
||||||
|
let lastText = "";
|
||||||
|
let stderrTail = "";
|
||||||
|
let buf = "";
|
||||||
|
child.stdout.on("data", (chunk) => {
|
||||||
|
buf += chunk;
|
||||||
|
let idx;
|
||||||
|
while ((idx = buf.indexOf("\n")) !== -1) {
|
||||||
|
const line = buf.slice(0, idx);
|
||||||
|
buf = buf.slice(idx + 1);
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
try {
|
||||||
|
const event = JSON.parse(line);
|
||||||
|
if (event.type === "message_end" && event.message && Array.isArray(event.message.content)) {
|
||||||
|
const text = event.message.content
|
||||||
|
.filter((c) => c.type === "text")
|
||||||
|
.map((c) => c.text)
|
||||||
|
.join("\n");
|
||||||
|
if (text) lastText = text;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// non-JSON stdout noise, ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
child.stderr.on("data", (chunk) => {
|
||||||
|
process.stderr.write(chunk);
|
||||||
|
stderrTail = (stderrTail + chunk.toString()).slice(-4000);
|
||||||
|
});
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
child.kill("SIGKILL");
|
||||||
|
resolve({ code: null, lastText, stderrTail, timedOut: true });
|
||||||
|
}, SPAWN_PI_TIMEOUT_MS);
|
||||||
|
child.on("close", (code) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve({ code, lastText, stderrTail });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function askResolver(cwd, repoId, diagnosticPrompt) {
|
||||||
|
const result = await spawnPi({ agent: "resolver", prompt: diagnosticPrompt, cwd });
|
||||||
|
return parseVerdictLine(result.lastText, "RESOLUTION");
|
||||||
|
}
|
||||||
|
|
||||||
|
const STAGE_TIMEOUT_MS = 10 * 60 * 1000;
|
||||||
|
const NUDGE_TIMEOUT_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
// Resolves as soon as filePath appears (fs.watch on its directory, same as
|
||||||
|
// before), as soon as target's agent-manager status flips to "errored" or
|
||||||
|
// "dead" (polled -- state.db has no watch mechanism), or after limitMs with
|
||||||
|
// neither. A session that has actually crashed will never touch the
|
||||||
|
// sentinel, so without the status poll this just burns the full STAGE_
|
||||||
|
// TIMEOUT_MS waiting on a file that was never coming, same as a genuine
|
||||||
|
// stall -- polling status catches that in ~pollMs instead.
|
||||||
|
function waitForSentinel(filePath, target, limitMs, pollMs = 5000) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (fs.existsSync(filePath)) return resolve({ ok: true });
|
||||||
|
const dir = path.dirname(filePath);
|
||||||
|
const id = target.replace(/^am_/, "");
|
||||||
|
let settled = false;
|
||||||
|
let watcher;
|
||||||
|
let poller;
|
||||||
|
let timer;
|
||||||
|
const finish = (result) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
clearInterval(poller);
|
||||||
|
if (watcher) {
|
||||||
|
try {
|
||||||
|
watcher.close();
|
||||||
|
} catch {
|
||||||
|
// already closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(result);
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
watcher = fs.watch(dir, () => {
|
||||||
|
if (fs.existsSync(filePath)) finish({ ok: true });
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// dir missing at watch time is a real bug elsewhere (cwd should
|
||||||
|
// already exist); surface it as a timeout rather than hang forever.
|
||||||
|
return finish({ timedOut: true });
|
||||||
|
}
|
||||||
|
// Closes the race between the existsSync check above and the watcher
|
||||||
|
// actually being attached.
|
||||||
|
if (fs.existsSync(filePath)) return finish({ ok: true });
|
||||||
|
poller = setInterval(async () => {
|
||||||
|
const { out } = await runCmd("sqlite3", [AGENT_MANAGER_DB, `SELECT status FROM sessions WHERE id='${id}'`]);
|
||||||
|
const status = out.trim();
|
||||||
|
if (status === "errored" || status === "dead") finish({ dead: true, status });
|
||||||
|
}, pollMs);
|
||||||
|
timer = setTimeout(() => finish({ timedOut: true }), limitMs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kills a session that's actually crashed (not just slow) and archives it
|
||||||
|
// in agent-manager's own DB so it stops showing up as a live, unattended
|
||||||
|
// pane -- otherwise every crash leaves an orphaned tmux session + state.db
|
||||||
|
// row behind permanently, identical to the manually-cleaned-up poiman-
|
||||||
|
// planner ghost session found earlier this same run.
|
||||||
|
async function killDeadSession(target) {
|
||||||
|
await runAmTmux(["kill-session", "-t", target]);
|
||||||
|
const id = target.replace(/^am_/, "");
|
||||||
|
await runCmd("sqlite3", [AGENT_MANAGER_DB, `UPDATE sessions SET archived=1 WHERE id='${id}'`]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs one task's worth of work on a persistent per-role agent: spawns the
|
||||||
|
// role's session the first time it's ever needed for this repo, sends every
|
||||||
|
// later prompt into that same tmux pane via send-keys -- prefixed with a
|
||||||
|
// `/new` first, so the pane and agent-manager session stay the same but the
|
||||||
|
// model starts that prompt with a clean conversation, no history carried
|
||||||
|
// over from whatever task this role last handled. pool is a plain object
|
||||||
|
// keyed by role name ("planner"/"investigator"/"implementer"/"judge"),
|
||||||
|
// shared across every task in a repo's pipeline (see runRepoPipeline) -- it
|
||||||
|
// IS the 4-agent pool, one entry per role, filled in lazily as each role
|
||||||
|
// gets its first task.
|
||||||
|
// A dead/errored session gets one respawn-and-retry (same prompt, fresh
|
||||||
|
// session) before this stage is abandoned -- matches resolver-SKILL.md's
|
||||||
|
// own documented contract of retrying a failed stage at most once.
|
||||||
|
const DEAD_SESSION_RETRIES = 1;
|
||||||
|
|
||||||
|
async function runOnPool(pool, cwd, repoId, role, prompt, sentinelFile) {
|
||||||
|
fs.rmSync(sentinelFile, { force: true });
|
||||||
|
const label = `${repoId}-${role}`;
|
||||||
|
|
||||||
|
// A pooled session's shell cwd drifts as it explores the repo (e.g. cd
|
||||||
|
// into a Rust workspace subdirectory to read source) and nothing resets
|
||||||
|
// it back between turns. Seen in practice: a repo whose own internal
|
||||||
|
// workspace folder is one letter off from the repo's own directory name
|
||||||
|
// ("poiman" the repo vs. "poimen" the crate workspace inside it) was
|
||||||
|
// enough for the agent to touch its sentinel one level off from where
|
||||||
|
// this function is watching for it -- coordinator waits out the full
|
||||||
|
// STAGE_TIMEOUT_MS for a file that already exists, just in the wrong
|
||||||
|
// place. State the absolute target directory and use absolute paths for
|
||||||
|
// every filesystem instruction, so there's nothing for the agent to get
|
||||||
|
// wrong by reasoning about a relative "current directory."
|
||||||
|
const cwdReminder = `Your working directory for this task is ${cwd} -- if your shell isn't already there, run: cd ${cwd}\n\n`;
|
||||||
|
|
||||||
|
const spawnFresh = async () => {
|
||||||
|
const spawnArgs = ["spawn", "--tool", "pi", "--cwd", cwd, "--name", label, "--group", repoId, "--prompt", cwdReminder + HARD_RULES + prompt];
|
||||||
|
const { provider, model } = providerModelFor(role);
|
||||||
|
if (provider) spawnArgs.push("--provider", provider);
|
||||||
|
if (model) spawnArgs.push("--model", model);
|
||||||
|
const spawned = await runCmd(AGENT_MANAGER_BIN, spawnArgs);
|
||||||
|
return spawned.code === 0 ? amSessionName(spawned.out) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let target = pool[role];
|
||||||
|
if (!target) {
|
||||||
|
target = await spawnFresh();
|
||||||
|
if (!target) return { ok: false, crashed: true, error: "spawn failed", sessionName: label };
|
||||||
|
pool[role] = target;
|
||||||
|
} else {
|
||||||
|
await runAmTmux(["send-keys", "-t", target, "/new", "Enter"]);
|
||||||
|
await sleep(1000);
|
||||||
|
await runAmTmux(["send-keys", "-t", target, cwdReminder + HARD_RULES + prompt, "Enter"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let deadRetries = 0; ; deadRetries++) {
|
||||||
|
const outcome = await waitForSentinel(sentinelFile, target, STAGE_TIMEOUT_MS);
|
||||||
|
if (outcome.ok) return { ok: true, sessionName: label };
|
||||||
|
|
||||||
|
if (outcome.dead) {
|
||||||
|
await killDeadSession(target);
|
||||||
|
if (pool[role] === target) delete pool[role];
|
||||||
|
if (deadRetries >= DEAD_SESSION_RETRIES) {
|
||||||
|
return { ok: false, crashed: true, error: `session died (status: ${outcome.status})`, sessionName: label };
|
||||||
|
}
|
||||||
|
target = await spawnFresh();
|
||||||
|
if (!target) return { ok: false, crashed: true, error: "respawn after death failed", sessionName: label };
|
||||||
|
pool[role] = target;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plain stall -- session still alive, just slow. Ask resolver once,
|
||||||
|
// nudge if it says worth it, and stop here either way (this is not
|
||||||
|
// the death path, so no respawn/retry loop).
|
||||||
|
const pane = await runAmTmux(["capture-pane", "-t", target, "-p", "-S", "-200"]);
|
||||||
|
const resolution = await askResolver(
|
||||||
|
cwd,
|
||||||
|
repoId,
|
||||||
|
`Repo ${repoId}'s "${role}" agent hasn't finished its current task after 10 minutes. Its pane tail:\n${pane.out.slice(-3000)}\n\n` +
|
||||||
|
`Decide: is it still making real progress and worth nudging to wrap up, or stuck and worth abandoning?`
|
||||||
|
);
|
||||||
|
let ok = false;
|
||||||
|
if (resolution === "RETRY") {
|
||||||
|
await runAmTmux(["send-keys", "-t", target, `Please wrap up now and run: touch ${sentinelFile}`, "Enter"]);
|
||||||
|
const nudged = await waitForSentinel(sentinelFile, target, NUDGE_TIMEOUT_MS);
|
||||||
|
ok = nudged.ok === true;
|
||||||
|
if (nudged.dead) {
|
||||||
|
await killDeadSession(target);
|
||||||
|
if (pool[role] === target) delete pool[role];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ok, sessionName: label };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function plannerPrompt(task, specHint, judgeOnly, cwd) {
|
||||||
|
// judgeOnly (auto-discovered tasks only, see parseTaskBoard): planner
|
||||||
|
// itself decides whether the task is already done before planning it,
|
||||||
|
// reading tasks/INDEX.md's own status notes plus git log/current code --
|
||||||
|
// replaces what used to be a separate judge pre-check call. One LLM round
|
||||||
|
// trip instead of two, and the same agent that's about to plan the task
|
||||||
|
// is the one deciding whether planning it is even necessary.
|
||||||
|
const resultFile = path.join(cwd, `.task-result-${task}`);
|
||||||
|
const decideStep = judgeOnly
|
||||||
|
? `First, decide whether task ${task} is already fully implemented on this branch: check ` +
|
||||||
|
`\`git log --oneline --grep '${task}'\`, tasks/INDEX.md's own status notes for this task, and the current ` +
|
||||||
|
`code directly against its spec (${specHint})'s acceptance criteria. Write your decision to ` +
|
||||||
|
`${resultFile} as a single "VERDICT: PASS" (already done, no further work needed) or ` +
|
||||||
|
`"VERDICT: FAIL" (needs work) line plus one line of rationale. If VERDICT is FAIL, continue below and ` +
|
||||||
|
`draft the plan in this same turn; if VERDICT is PASS, skip the rest and go straight to the touch step.\n\n`
|
||||||
|
: "";
|
||||||
|
return (
|
||||||
|
`${decideStep}Use the planner skill to draft PLAN.md for task ${task}, reading its spec (${specHint}). ` +
|
||||||
|
`PLAN.md is scratch state for this harness, not a deliverable -- do NOT commit it or add it to git. ` +
|
||||||
|
`Then run: touch ${path.join(cwd, `.stage-done-${task}-planner`)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function investigatorPrompt(task, cwd) {
|
||||||
|
return (
|
||||||
|
`Use the investigator skill to confirm PLAN.md against real sources for task ${task}, append findings. ` +
|
||||||
|
`PLAN.md is scratch state for this harness, not a deliverable -- do NOT commit it or add it to git. ` +
|
||||||
|
`Then run: touch ${path.join(cwd, `.stage-done-${task}-investigator`)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function implementerPrompt(task, attempt, feedbackHint, cwd) {
|
||||||
|
return (
|
||||||
|
`Use the implementer skill to implement what the current PLAN.md specifies for task ${task} (commit as you go). ` +
|
||||||
|
`${feedbackHint} Then run: touch ${path.join(cwd, `.stage-done-${task}-implementer-${attempt}`)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function judgePrompt(task, baseBranch, attempt, cwd) {
|
||||||
|
return (
|
||||||
|
`${REQUIRE_REAL_TOOL_CALLS}Use the judge skill to review the diff against ${baseBranch}...HEAD for task ${task}. ` +
|
||||||
|
`Write your verdict to ${path.join(cwd, `.task-result-${task}`)} as a single "VERDICT: PASS" or "VERDICT: FAIL" ` +
|
||||||
|
`line plus one line of rationale, then run: touch ${path.join(cwd, `.stage-done-${task}-judge-${attempt}`)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_IMPLEMENT_ATTEMPTS = 5;
|
||||||
|
const MAX_PLAN_REVISIONS = 3;
|
||||||
|
|
||||||
|
// Runs one task against the repo's shared role pool: planner drafts
|
||||||
|
// PLAN.md (for auto-discovered tasks, first deciding off tasks/INDEX.md and
|
||||||
|
// the repo's own state whether the task is already done -- see
|
||||||
|
// plannerPrompt's judgeOnly branch; judge never does this pre-check),
|
||||||
|
// investigator confirms it, then implementer and judge go back and
|
||||||
|
// forth -- judge's FAIL rationale lands in .task-result-<task>, which the
|
||||||
|
// next implementer attempt is told to read and address. After
|
||||||
|
// MAX_IMPLEMENT_ATTEMPTS straight fails, the planner role is asked to judge
|
||||||
|
// whether the plan itself is wrong -- fresh conversation, same as any other
|
||||||
|
// planner call, reading PLAN.md/the judge feedback/the
|
||||||
|
// diff off disk rather than remembering having drafted the original plan.
|
||||||
|
// If it decides the approach is wrong it revises PLAN.md and the implementer
|
||||||
|
// gets a fresh attempt budget.
|
||||||
|
// MAX_PLAN_REVISIONS caps this from looping forever on a task that's
|
||||||
|
// genuinely stuck. All work happens directly in cwd (the repo's one shared
|
||||||
|
// clone, currently checked out to the phase branch) -- no worktree, since
|
||||||
|
// only one implementer/judge exist per repo and tasks run strictly one at a
|
||||||
|
// time (see runPhase).
|
||||||
|
async function runTaskOnPool(cwd, baseBranch, task, pool, repoId, pipelineSession, judgeOnly) {
|
||||||
|
const resultFile = path.join(cwd, `.task-result-${task}`);
|
||||||
|
fs.rmSync(resultFile, { force: true });
|
||||||
|
|
||||||
|
const specHint = `the file under tasks/ starting with "${task}-"`;
|
||||||
|
|
||||||
|
const stage = async (role, prompt, sentinel, displayLabel) => {
|
||||||
|
const label = displayLabel || role;
|
||||||
|
pipelineSession.activeTasks[task] = { stage: label, startedAt: new Date().toISOString() };
|
||||||
|
logProgress(pipelineSession);
|
||||||
|
const result = await runOnPool(pool, cwd, repoId, role, prompt, sentinel);
|
||||||
|
await commitPending(cwd, `task: ${task} (${label})`);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const abandon = (stageLabel, result, attempt) => {
|
||||||
|
delete pipelineSession.activeTasks[task];
|
||||||
|
logProgress(pipelineSession);
|
||||||
|
return {
|
||||||
|
task,
|
||||||
|
status: result.crashed ? "spawn-crashed" : "timed-out",
|
||||||
|
error: result.error,
|
||||||
|
stoppedAt: stageLabel,
|
||||||
|
...(attempt !== undefined ? { attempt } : {}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// PLAN.md is scratch state for this one task, not a deliverable (see
|
||||||
|
// plannerPrompt/investigatorPrompt -- it's gitignored too, as a backstop
|
||||||
|
// in case an agent commits it anyway). Discard it once the task is done,
|
||||||
|
// whatever the outcome, so it never bleeds into the next task's planner
|
||||||
|
// call or sits around as stale harness clutter in the shared clone.
|
||||||
|
try {
|
||||||
|
let result = await stage("planner", plannerPrompt(task, specHint, judgeOnly, cwd), path.join(cwd, `.stage-done-${task}-planner`));
|
||||||
|
if (!result.ok) return abandon("planner", result);
|
||||||
|
|
||||||
|
if (judgeOnly) {
|
||||||
|
const quickText = fs.existsSync(resultFile) ? fs.readFileSync(resultFile, "utf8") : "";
|
||||||
|
if (parseVerdictLine(quickText, "VERDICT") === "PASS") {
|
||||||
|
delete pipelineSession.activeTasks[task];
|
||||||
|
logProgress(pipelineSession);
|
||||||
|
return { task, status: "done", judgeRationale: quickText, judgeOnlyPass: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await stage("investigator", investigatorPrompt(task, cwd), path.join(cwd, `.stage-done-${task}-investigator`));
|
||||||
|
if (!result.ok) return abandon("investigator", result);
|
||||||
|
|
||||||
|
let planRevisions = 0;
|
||||||
|
let implementAttempt = 0;
|
||||||
|
let verdict = null;
|
||||||
|
let resultText = "";
|
||||||
|
let justRevisedPlan = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
implementAttempt++;
|
||||||
|
const feedbackHint = fs.existsSync(resultFile)
|
||||||
|
? justRevisedPlan
|
||||||
|
? `${resultFile} holds the judge's feedback against the OLD plan, which prompted a plan revision -- ` +
|
||||||
|
`PLAN.md has since changed. Read the current PLAN.md as the source of truth, not the old feedback verbatim.`
|
||||||
|
: `A previous judge review exists at ${resultFile} -- read it and address every issue it raises.`
|
||||||
|
: "";
|
||||||
|
justRevisedPlan = false;
|
||||||
|
|
||||||
|
result = await stage(
|
||||||
|
"implementer",
|
||||||
|
implementerPrompt(task, implementAttempt, feedbackHint, cwd),
|
||||||
|
path.join(cwd, `.stage-done-${task}-implementer-${implementAttempt}`)
|
||||||
|
);
|
||||||
|
if (!result.ok) return abandon("implementer", result, implementAttempt);
|
||||||
|
|
||||||
|
result = await stage("judge", judgePrompt(task, baseBranch, implementAttempt, cwd), path.join(cwd, `.stage-done-${task}-judge-${implementAttempt}`));
|
||||||
|
if (!result.ok) return abandon("judge", result, implementAttempt);
|
||||||
|
|
||||||
|
resultText = fs.existsSync(resultFile) ? fs.readFileSync(resultFile, "utf8") : "";
|
||||||
|
verdict = parseVerdictLine(resultText, "VERDICT");
|
||||||
|
if (verdict === "PASS") break;
|
||||||
|
|
||||||
|
if (implementAttempt >= MAX_IMPLEMENT_ATTEMPTS) {
|
||||||
|
if (planRevisions >= MAX_PLAN_REVISIONS) break;
|
||||||
|
planRevisions++;
|
||||||
|
result = await stage(
|
||||||
|
"planner",
|
||||||
|
`Implementer failed judge review ${MAX_IMPLEMENT_ATTEMPTS} times in a row for task ${task}. Read PLAN.md, ` +
|
||||||
|
`the judge's feedback in ${resultFile}, and the current diff against ${baseBranch}...HEAD. Decide ` +
|
||||||
|
`whether the plan's approach itself is wrong, not just the implementation -- if so, revise PLAN.md. If ` +
|
||||||
|
`you change the approach, also use the investigator skill to confirm the new approach against real ` +
|
||||||
|
`sources. If the plan is sound, note why in PLAN.md and leave it as-is. PLAN.md is scratch state for ` +
|
||||||
|
`this harness, not a deliverable -- do NOT commit it or add it to git. Then run: ` +
|
||||||
|
`touch ${path.join(cwd, `.stage-done-${task}-planner-revise-${planRevisions}`)}`,
|
||||||
|
path.join(cwd, `.stage-done-${task}-planner-revise-${planRevisions}`),
|
||||||
|
"planner-revise"
|
||||||
|
);
|
||||||
|
if (!result.ok) return abandon("planner-revise", result, planRevisions);
|
||||||
|
implementAttempt = 0;
|
||||||
|
justRevisedPlan = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete pipelineSession.activeTasks[task];
|
||||||
|
logProgress(pipelineSession);
|
||||||
|
|
||||||
|
if (verdict !== "PASS" && planRevisions >= MAX_PLAN_REVISIONS) {
|
||||||
|
return { task, status: "unresolved", judgeRationale: resultText, implementAttempts: implementAttempt, planRevisions };
|
||||||
|
}
|
||||||
|
return { task, status: verdict === "PASS" ? "done" : "done-with-concerns", judgeRationale: resultText };
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(path.join(cwd, "PLAN.md"), { force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Committed (never gitignored) so it survives a resumed phase branch --
|
||||||
|
// one task id per line, appended as each task resolves. This is what lets
|
||||||
|
// a resumed run skip straight past already-resolved tasks instead of
|
||||||
|
// re-running planner's judgeOnly decision on every one of them again:
|
||||||
|
// resuming the git branch alone only recovers the CODE, not "which tasks
|
||||||
|
// are already settled," and re-deciding that from scratch for every task
|
||||||
|
// burns a full LLM call per already-done task before ever reaching the
|
||||||
|
// first one that actually needs work.
|
||||||
|
function progressLedgerPath(cwd) {
|
||||||
|
return path.join(cwd, ".agent-progress");
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCompletedTasks(cwd) {
|
||||||
|
const file = progressLedgerPath(cwd);
|
||||||
|
if (!fs.existsSync(file)) return new Set();
|
||||||
|
return new Set(
|
||||||
|
fs
|
||||||
|
.readFileSync(file, "utf8")
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recordTaskComplete(cwd, task) {
|
||||||
|
fs.appendFileSync(progressLedgerPath(cwd), `${task}\n`);
|
||||||
|
await runGit(cwd, ["add", path.basename(progressLedgerPath(cwd))]);
|
||||||
|
await runGit(cwd, ["commit", "-m", `chore: mark ${task} complete in progress ledger`]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs every task in a phase (no declared dependency between them) strictly
|
||||||
|
// one at a time against the repo's shared role pool -- only one implementer/
|
||||||
|
// judge/etc. exists per repo, so there is no per-task concurrency to have
|
||||||
|
// here anymore (see REPO_CONCURRENCY below for where concurrency now
|
||||||
|
// lives). No worktrees: every task commits directly onto phaseBranch in the
|
||||||
|
// one shared cwd. baseBranch here is the TRUE base (e.g. "main") -- judge
|
||||||
|
// reviews `git diff baseBranch...HEAD`, not phaseBranch...HEAD, which would
|
||||||
|
// always be empty since HEAD *is* phaseBranch while it's checked out.
|
||||||
|
//
|
||||||
|
// Pushes phaseBranch after every task, not just once at full-phase-end: the
|
||||||
|
// pod is ephemeral and every restart re-clones baseBranch fresh (see
|
||||||
|
// runRepoPipeline) -- without this, a redeploy mid-phase silently discards
|
||||||
|
// every task committed so far, and the next run re-decides "is this done?"
|
||||||
|
// from a clone that never saw any of that work.
|
||||||
|
async function runPhase(cwd, baseBranch, phaseBranch, phaseTasks, pool, repoId, pipelineSession) {
|
||||||
|
const entries = phaseTasks.map((t) => (typeof t === "string" ? { id: t, judgeOnly: false } : t));
|
||||||
|
const completed = readCompletedTasks(cwd);
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (completed.has(entry.id)) {
|
||||||
|
const result = { task: entry.id, status: "done", resumed: true };
|
||||||
|
pipelineSession.taskResults.push(result);
|
||||||
|
logProgress(pipelineSession);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await runTaskOnPool(cwd, baseBranch, entry.id, pool, repoId, pipelineSession, entry.judgeOnly);
|
||||||
|
pipelineSession.taskResults.push(result);
|
||||||
|
if (result.status === "done" || result.status === "done-with-concerns") {
|
||||||
|
await recordTaskComplete(cwd, entry.id);
|
||||||
|
}
|
||||||
|
await runGit(cwd, ["push", "-u", "origin", phaseBranch]);
|
||||||
|
logProgress(pipelineSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discovers phases/tasks from the repo's own tasks/INDEX.md instead of
|
||||||
|
// requiring the caller to pass --tasks. Matches this convention's board
|
||||||
|
// shape (see e.g. Poimen/agent-rust's tasks/INDEX.md): a numbered phase
|
||||||
|
// heading ("## 1 — Foundations · T0.x"), followed by a markdown table
|
||||||
|
// whose rows link to each task's own spec file ("| [T0.1](T0.1-....md) |
|
||||||
|
// ... |"). Headings that aren't a numbered phase (prose sections like
|
||||||
|
// "## Ordering — declared, never derived", "## Progress") are skipped --
|
||||||
|
// only "## <digits> — ..." starts a new phase. Returns null if
|
||||||
|
// tasks/INDEX.md doesn't exist; an empty array if it exists but no phase
|
||||||
|
// yielded any task rows.
|
||||||
|
function parseTaskBoard(cwd) {
|
||||||
|
const indexPath = path.join(cwd, "tasks", "INDEX.md");
|
||||||
|
if (!fs.existsSync(indexPath)) return null;
|
||||||
|
|
||||||
|
const phaseHeaderRe = /^##\s+\d+\s+—/;
|
||||||
|
const taskRowRe = /^\|\s*\[([A-Za-z0-9.]+)\]\(/;
|
||||||
|
|
||||||
|
const phases = [];
|
||||||
|
let current = null;
|
||||||
|
for (const line of fs.readFileSync(indexPath, "utf8").split("\n")) {
|
||||||
|
if (phaseHeaderRe.test(line)) {
|
||||||
|
current = [];
|
||||||
|
phases.push(current);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const m = line.match(taskRowRe);
|
||||||
|
if (m && current) current.push(m[1]);
|
||||||
|
}
|
||||||
|
return phases.filter((phase) => phase.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function phaseLabelFor(phaseTasks, index) {
|
||||||
|
const first = phaseTasks[0];
|
||||||
|
const id = typeof first === "string" ? first : first.id;
|
||||||
|
const dot = id.indexOf(".");
|
||||||
|
return dot === -1 ? `phase-${index}` : id.slice(0, dot);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logProgress(pipelineSession) {
|
||||||
|
console.log(`[repo ${pipelineSession.id}] ${JSON.stringify(pipelineSession)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs one repo's full pipeline: clone, then phases strictly sequentially.
|
||||||
|
// tasks: array of phases, each phase an array of task ids with no declared
|
||||||
|
// dependency on each other (e.g. [["T0.1","T0.2"], ["T1.1","T1.2","T1.3"]]).
|
||||||
|
// A flat array of ids is also accepted and treated as one single phase. If
|
||||||
|
// omitted, phases are discovered from the repo's own tasks/INDEX.md and run
|
||||||
|
// judgeOnly first (a cheap "is this already done" check against the
|
||||||
|
// board's possibly-stale checkmarks). Each phase gets its own branch
|
||||||
|
// (agent-run/<repoId>/<phaseLabel>, e.g. .../T1); once every task in that
|
||||||
|
// phase lands "done" or "done-with-concerns" AND the phase judge (the same
|
||||||
|
// pooled judge agent that reviewed each task) passes the integration
|
||||||
|
// review, the phase branch is squash-merged into baseBranch and pushed,
|
||||||
|
// then the next phase branches off that updated base. Any failure halts
|
||||||
|
// this repo's pipeline before merging -- it does not affect other repos
|
||||||
|
// running concurrently (see runCoordinator).
|
||||||
|
async function runRepoPipeline({ repoId, repo, baseBranch, tasks, branchName }, pipelineSession) {
|
||||||
|
const cwd = path.join(WORK_DIR, repoId);
|
||||||
|
// repoId is a slug derived from the repo URL now (see slugFor), not a
|
||||||
|
// fresh UUID -- reusable across separate `runCoordinator` invocations
|
||||||
|
// against the same repo, so a stale clone from a prior run has to be
|
||||||
|
// wiped before this one starts, not merged into.
|
||||||
|
fs.rmSync(cwd, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(cwd, { recursive: true });
|
||||||
|
const pool = {};
|
||||||
|
|
||||||
|
const finish = (status) => {
|
||||||
|
pipelineSession.status = status;
|
||||||
|
pipelineSession.endedAt = new Date().toISOString();
|
||||||
|
logProgress(pipelineSession);
|
||||||
|
return pipelineSession;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Deterministic, not routed through an LLM -- clone is 100% mechanical
|
||||||
|
// (same reasoning as commitPending/the squash-merge sequence below), and
|
||||||
|
// was the one place left that broke that pattern: a headless spawnPi
|
||||||
|
// call here meant a crash gave zero diagnostic output, just a silent
|
||||||
|
// exit code with nothing to debug from.
|
||||||
|
const clone = await runGit(cwd, ["clone", "--branch", baseBranch, repo, "."]);
|
||||||
|
if (clone.code !== 0) {
|
||||||
|
pipelineSession.gitError = clone.out;
|
||||||
|
return finish("clone-crashed");
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(path.join(cwd, ".git"))) return finish("clone-missing");
|
||||||
|
|
||||||
|
let phases = tasks ? (Array.isArray(tasks[0]) ? tasks : [tasks]) : parseTaskBoard(cwd);
|
||||||
|
if (!phases || phases.length === 0) {
|
||||||
|
pipelineSession.gitError = "no tasks given and tasks/INDEX.md not found or empty";
|
||||||
|
return finish("no-tasks-found");
|
||||||
|
}
|
||||||
|
if (!tasks) {
|
||||||
|
phases = phases.map((phase) => phase.map((id) => ({ id, judgeOnly: true })));
|
||||||
|
}
|
||||||
|
pipelineSession.totalTasks = phases.flat().length;
|
||||||
|
|
||||||
|
for (let i = 0; i < phases.length; i++) {
|
||||||
|
const phaseTasks = phases[i];
|
||||||
|
const phaseLabel = phaseLabelFor(phaseTasks, i);
|
||||||
|
const phaseBranch = branchName ? `${branchName}/${phaseLabel}` : `agent-run/${repoId}/${phaseLabel}`;
|
||||||
|
|
||||||
|
// Resume a phase branch a prior (since-restarted) run already pushed,
|
||||||
|
// instead of always branching fresh off baseBranch -- otherwise every
|
||||||
|
// redeploy silently discards whatever tasks that prior run already
|
||||||
|
// committed and pushed (see runPhase's per-task push below).
|
||||||
|
const fetchExisting = await runGit(cwd, ["fetch", "origin", phaseBranch]);
|
||||||
|
const resuming = fetchExisting.code === 0;
|
||||||
|
const branchResult = resuming
|
||||||
|
? await runGit(cwd, ["checkout", "-b", phaseBranch, "FETCH_HEAD"])
|
||||||
|
: await runGit(cwd, ["checkout", "-b", phaseBranch]);
|
||||||
|
if (branchResult.code !== 0) {
|
||||||
|
pipelineSession.gitError = branchResult.out;
|
||||||
|
return finish("branch-crashed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i === 0 && !resuming) {
|
||||||
|
const gitignoreAdditions = [
|
||||||
|
"",
|
||||||
|
"# agent-harness: build artifacts and vendored archives never belong in source control",
|
||||||
|
"*.tar.gz",
|
||||||
|
"*.tgz",
|
||||||
|
"*.crate",
|
||||||
|
"*.zip",
|
||||||
|
"*.bin",
|
||||||
|
"*.whl",
|
||||||
|
"vendor/",
|
||||||
|
"node_modules/",
|
||||||
|
"",
|
||||||
|
"# agent-harness: task/phase completion sentinel files, harness bookkeeping only",
|
||||||
|
".task-result-*",
|
||||||
|
".phase-result-*",
|
||||||
|
".stage-done-*",
|
||||||
|
"",
|
||||||
|
"# agent-harness: PLAN.md is per-task planner scratch state, never a deliverable",
|
||||||
|
"PLAN.md",
|
||||||
|
].join("\n");
|
||||||
|
fs.appendFileSync(path.join(cwd, ".gitignore"), gitignoreAdditions + "\n");
|
||||||
|
await runGit(cwd, ["add", ".gitignore"]);
|
||||||
|
await runGit(cwd, ["commit", "-m", "chore: broaden .gitignore for agent-run artifacts"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
await runPhase(cwd, baseBranch, phaseBranch, phaseTasks, pool, repoId, pipelineSession);
|
||||||
|
|
||||||
|
const phaseTaskIds = new Set(phaseTasks.map((t) => (typeof t === "string" ? t : t.id)));
|
||||||
|
const phaseResults = pipelineSession.taskResults.filter((r) => phaseTaskIds.has(r.task));
|
||||||
|
const phaseClean =
|
||||||
|
phaseResults.length === phaseTaskIds.size && phaseResults.every((r) => r.status === "done" || r.status === "done-with-concerns");
|
||||||
|
|
||||||
|
if (!phaseClean) {
|
||||||
|
pipelineSession.haltedAt = phaseLabel;
|
||||||
|
return finish("halted-phase-failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const phaseResultFile = path.join(cwd, `.phase-result-${phaseLabel}`);
|
||||||
|
fs.rmSync(phaseResultFile, { force: true });
|
||||||
|
const phaseJudge = await runOnPool(
|
||||||
|
pool,
|
||||||
|
cwd,
|
||||||
|
repoId,
|
||||||
|
"judge",
|
||||||
|
`${REQUIRE_REAL_TOOL_CALLS}Use the judge skill to review the full phase diff for phase ${phaseLabel} against ` +
|
||||||
|
`${baseBranch}...HEAD (covers every task in this phase: ${[...phaseTaskIds].join(", ")}). Every ` +
|
||||||
|
`individual task already passed its own judge review -- your job here is different: confirm the ` +
|
||||||
|
`tasks integrate correctly as one coherent narrative, and that real integration tests (not just ` +
|
||||||
|
`each task's isolated unit checks) exist and actually exercise the phase's intended use case end ` +
|
||||||
|
`to end. Write your verdict to ${phaseResultFile} as a single "VERDICT: PASS" or ` +
|
||||||
|
`"VERDICT: FAIL" line plus rationale, then run: touch ${path.join(cwd, `.stage-done-phase-${phaseLabel}-judge`)}`,
|
||||||
|
path.join(cwd, `.stage-done-phase-${phaseLabel}-judge`)
|
||||||
|
);
|
||||||
|
await commitPending(cwd, `phase: ${phaseLabel} integration review`);
|
||||||
|
|
||||||
|
if (!phaseJudge.ok) {
|
||||||
|
pipelineSession.haltedAt = phaseLabel;
|
||||||
|
pipelineSession.gitError = phaseJudge.error;
|
||||||
|
return finish("phase-judge-crashed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const phaseJudgeText = fs.existsSync(phaseResultFile) ? fs.readFileSync(phaseResultFile, "utf8") : "";
|
||||||
|
if (parseVerdictLine(phaseJudgeText, "VERDICT") !== "PASS") {
|
||||||
|
pipelineSession.haltedAt = phaseLabel;
|
||||||
|
pipelineSession.phaseJudgeRationale = phaseJudgeText;
|
||||||
|
return finish("halted-phase-judge-failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkoutBase = await runGit(cwd, ["checkout", baseBranch]);
|
||||||
|
if (checkoutBase.code !== 0) {
|
||||||
|
pipelineSession.gitError = checkoutBase.out;
|
||||||
|
return finish("squash-crashed");
|
||||||
|
}
|
||||||
|
const squash = await runGit(cwd, ["merge", "--squash", phaseBranch]);
|
||||||
|
if (squash.code !== 0) {
|
||||||
|
await runGit(cwd, ["merge", "--abort"]);
|
||||||
|
pipelineSession.gitError = squash.out;
|
||||||
|
return finish("squash-crashed");
|
||||||
|
}
|
||||||
|
const commit = await runGit(cwd, ["commit", "-m", `feat: ${phaseLabel} (${[...phaseTaskIds].join(", ")})`]);
|
||||||
|
if (commit.code !== 0) {
|
||||||
|
pipelineSession.gitError = commit.out;
|
||||||
|
return finish("squash-crashed");
|
||||||
|
}
|
||||||
|
const push = await runGit(cwd, ["push", "origin", baseBranch]);
|
||||||
|
if (push.code !== 0) {
|
||||||
|
pipelineSession.gitError = push.out;
|
||||||
|
return finish("squash-push-crashed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Milestone's content now lives in baseBranch as one squashed commit --
|
||||||
|
// the phase branch (and whatever a prior restart already pushed of it)
|
||||||
|
// has no further reason to exist. Delete it both places so a future run
|
||||||
|
// never tries to resume a phase that's already done, and so origin
|
||||||
|
// doesn't accumulate one dangling branch per completed phase forever.
|
||||||
|
await runGit(cwd, ["branch", "-D", phaseBranch]);
|
||||||
|
await runGit(cwd, ["push", "origin", "--delete", phaseBranch]);
|
||||||
|
|
||||||
|
logProgress(pipelineSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
return finish("completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runConcurrent(items, limit, worker) {
|
||||||
|
const results = new Array(items.length);
|
||||||
|
let i = 0;
|
||||||
|
async function next() {
|
||||||
|
while (i < items.length) {
|
||||||
|
const idx = i++;
|
||||||
|
results[idx] = await worker(items[idx], idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, next));
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// How many repos can be mid-flight at once. Each repo gets its own clone
|
||||||
|
// and its own 4-agent pool (planner/investigator/implementer/judge), so
|
||||||
|
// this is now the real concurrency knob -- tasks within one repo are
|
||||||
|
// already serialized against that repo's pool (see runPhase). The backend
|
||||||
|
// (homelab-ornith) actually runs 2 GPU replicas behind one Kubernetes
|
||||||
|
// Service, each with its own copy of the model loaded (see homelab's
|
||||||
|
// k8s/apps/llm-serving/ornith.yaml) -- so up to 2 concurrent LLM calls get
|
||||||
|
// real independent instances; a 3rd+ concurrent call queues inside
|
||||||
|
// whichever replica the Service's own load-balancing lands it on (each
|
||||||
|
// replica runs OLLAMA_NUM_PARALLEL=1). REPO_CONCURRENCY above 2 is still
|
||||||
|
// useful (more repos in flight overlaps git/file work, not just LLM calls)
|
||||||
|
// but past 2 simultaneous LLM calls, extra concurrency mostly means queueing
|
||||||
|
// rather than added throughput -- bump the backend's replica count to
|
||||||
|
// change that, not this constant.
|
||||||
|
const REPO_CONCURRENCY = Number(process.env.REPO_CONCURRENCY) || 3;
|
||||||
|
|
||||||
|
// repoId is the repo's own name, not a random id -- it's what every role
|
||||||
|
// session's --name is built from (see runOnPool: `${repoId}-${role}`), so
|
||||||
|
// agent-manager's own session list groups naturally by repo ("portfolio-
|
||||||
|
// planner", "portfolio-judge", "poiman-planner", ...) instead of by opaque
|
||||||
|
// UUID. Takes the last path segment of the URL, strips a trailing `.git`,
|
||||||
|
// and sanitizes anything that isn't safe in a tmux session name / directory
|
||||||
|
// name / git branch name. Two different repos that happen to share a
|
||||||
|
// basename (e.g. two orgs' "portfolio") would collide -- not handled, since
|
||||||
|
// nothing about this harness's usage has needed more than one org per run.
|
||||||
|
function slugFor(repoUrl) {
|
||||||
|
const last = repoUrl.replace(/\/+$/, "").split("/").pop() || repoUrl;
|
||||||
|
return last.replace(/\.git$/, "").replace(/[^a-zA-Z0-9._-]/g, "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top-level entry point: runs every repo in `repos` to completion, up to
|
||||||
|
// REPO_CONCURRENCY at a time. Returns a map of repoId -> final
|
||||||
|
// pipelineSession, one per repo, independent of how the others fared.
|
||||||
|
async function runCoordinator({ repos, base, tasks, branchName }) {
|
||||||
|
const sessions = {};
|
||||||
|
await runConcurrent(repos, REPO_CONCURRENCY, async (repoUrl) => {
|
||||||
|
const repoId = slugFor(repoUrl);
|
||||||
|
const pipelineSession = {
|
||||||
|
id: repoId,
|
||||||
|
repo: repoUrl,
|
||||||
|
status: "running",
|
||||||
|
taskResults: [],
|
||||||
|
activeTasks: {},
|
||||||
|
totalTasks: 0,
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
sessions[repoId] = pipelineSession;
|
||||||
|
await runRepoPipeline({ repoId, repo: repoUrl, baseBranch: base, tasks, branchName }, pipelineSession);
|
||||||
|
});
|
||||||
|
return sessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const opts = { base: "main" };
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const a = argv[i];
|
||||||
|
if (a === "--repo") opts.repo = argv[++i];
|
||||||
|
else if (a === "--repos") opts.repos = argv[++i];
|
||||||
|
else if (a === "--base") opts.base = argv[++i];
|
||||||
|
else if (a === "--tasks") opts.tasks = argv[++i];
|
||||||
|
else if (a === "--branch") opts.branch = argv[++i];
|
||||||
|
}
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const opts = parseArgs(process.argv.slice(2));
|
||||||
|
const repos = opts.repos ? opts.repos.split(",") : opts.repo ? [opts.repo] : null;
|
||||||
|
if (!repos || repos.length === 0) {
|
||||||
|
console.error(
|
||||||
|
"usage: coordinator.js --repos <url1,url2,...> [--tasks T0.1,T0.2;T1.1,T1.2,...] [--base main] [--branch <name>]\n" +
|
||||||
|
" --repo <url> also accepted for a single repo\n" +
|
||||||
|
" --tasks applies to every repo listed; omitted: each repo discovers its own phases from tasks/INDEX.md\n" +
|
||||||
|
" REPO_CONCURRENCY env var (default 3): how many repos run at once"
|
||||||
|
);
|
||||||
|
// process.exitCode + natural exit, not process.exit() -- stdout piped
|
||||||
|
// through kubectl exec (not a TTY) can drop buffered console.log/
|
||||||
|
// console.error output if the process exits before it flushes. Setting
|
||||||
|
// exitCode and letting the event loop drain naturally is the
|
||||||
|
// documented-safe way to exit with a specific code without racing it.
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const phases = opts.tasks ? opts.tasks.split(";").map((phase) => phase.split(",")) : null;
|
||||||
|
const sessions = await runCoordinator({ repos, base: opts.base, tasks: phases, branchName: opts.branch });
|
||||||
|
process.exitCode = Object.values(sessions).every((s) => s.status === "completed") ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { runCoordinator, runRepoPipeline, runOnPool, parseTaskBoard };
|
||||||
|
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: agent-pod
|
||||||
|
namespace: agent-pod
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: agent-pod
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: agent-pod
|
||||||
|
spec:
|
||||||
|
# api.riotpiao.com has no in-cluster DNS record (only resolves from the
|
||||||
|
# home network's own resolver) -- pin it to ingress-nginx-controller's
|
||||||
|
# ClusterIP so pi's models.json baseUrl works unchanged. TLS still
|
||||||
|
# terminates correctly since SNI/Host still say api.riotpiao.com.
|
||||||
|
hostAliases:
|
||||||
|
- ip: "10.101.128.185"
|
||||||
|
hostnames:
|
||||||
|
- "api.riotpiao.com"
|
||||||
|
containers:
|
||||||
|
# hub.js runs in the same container as pi (not a sidecar) so it can
|
||||||
|
# spawn `pi -p --mode json` directly via child_process -- a separate
|
||||||
|
# container can't exec into another container's filesystem/PATH.
|
||||||
|
# It IS the container's long-running process now; no more `sleep
|
||||||
|
# infinity` placeholder.
|
||||||
|
#
|
||||||
|
# Also builds the agent-manager fork (github.com/Riotpiaole/
|
||||||
|
# agent-manager, add-headless-spawn branch) from source and drops
|
||||||
|
# coordinator.js in beside hub.js -- neither is the container's
|
||||||
|
# foreground process. hub.js keeps that role unchanged; coordinator.js
|
||||||
|
# itself now owns multi-repo concurrency (REPO_CONCURRENCY env,
|
||||||
|
# default 3), so one invocation handles every repo:
|
||||||
|
# `kubectl exec <pod> -- node /root/coordinator.js --repos
|
||||||
|
# repoA,repoB,... --tasks ...`. Each repo gets its own clone and its
|
||||||
|
# own persistent 4-agent pool (planner/investigator/implementer/
|
||||||
|
# judge, one agent-manager session per role, reused across every
|
||||||
|
# task in that repo) on the container's local tmux server --
|
||||||
|
# `kubectl exec -it <pod> -- agent-manager` attaches its TUI live
|
||||||
|
# against those same sessions, no cross-machine visibility problem
|
||||||
|
# since spawner, tmux server, and viewer are all colocated here.
|
||||||
|
#
|
||||||
|
# No prebuilt Linux binary is shipped for agent-manager: the local
|
||||||
|
# .bin/ build is macOS arm64 (wrong OS/arch for this container
|
||||||
|
# anyway) and it's 27MB, well over a ConfigMap's ~1MiB cap. Debian's
|
||||||
|
# `apt-get golang-go` is far too old for this fork's go 1.26.5
|
||||||
|
# requirement, so the real Go toolchain is fetched directly from
|
||||||
|
# go.dev instead.
|
||||||
|
- name: pi
|
||||||
|
image: node:22-slim
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
apt-get update && apt-get install -y git curl jq openssh-client tmux python3 sqlite3 gcc build-essential
|
||||||
|
ssh-keygen -y -f /root/.ssh/id_forgejo > /root/.ssh/id_forgejo.pub
|
||||||
|
eval "$(ssh-agent -s)"
|
||||||
|
ssh-add /root/.ssh/id_forgejo
|
||||||
|
npm install -g @earendil-works/[email protected]
|
||||||
|
npm install --prefix /root ws
|
||||||
|
|
||||||
|
curl -fsSL "https://go.dev/dl/go1.26.5.linux-$(dpkg --print-architecture).tar.gz" | tar -C /usr/local -xz
|
||||||
|
export PATH="$PATH:/usr/local/go/bin"
|
||||||
|
git clone --branch add-headless-spawn --depth 1 \
|
||||||
|
https://github.com/Riotpiaole/agent-manager.git /root/agent-manager-src
|
||||||
|
(cd /root/agent-manager-src && go build -o /usr/local/bin/agent-manager .)
|
||||||
|
|
||||||
|
# Language toolchains for whatever repos the implementer/investigator/
|
||||||
|
# judge roles actually build and test -- go was already fetched above
|
||||||
|
# only for building agent-manager itself, and its PATH export above is
|
||||||
|
# local to this script, invisible to `kubectl exec` sessions into the
|
||||||
|
# already-running container. Symlinking both into /usr/local/bin (on
|
||||||
|
# PATH for every exec session, interactive or not) instead of relying
|
||||||
|
# on shell rc sourcing, which pi's non-interactive tool calls don't do.
|
||||||
|
ln -sf /usr/local/go/bin/go /usr/local/bin/go
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
||||||
|
ln -sf /root/.cargo/bin/cargo /usr/local/bin/cargo
|
||||||
|
ln -sf /root/.cargo/bin/rustc /usr/local/bin/rustc
|
||||||
|
ln -sf /root/.cargo/bin/rustup /usr/local/bin/rustup
|
||||||
|
|
||||||
|
node /root/hub.js
|
||||||
|
env:
|
||||||
|
- name: PI_BIN
|
||||||
|
value: pi
|
||||||
|
- name: AGENT_MANAGER_BIN
|
||||||
|
value: /usr/local/bin/agent-manager
|
||||||
|
- name: HUB_WORK_DIR
|
||||||
|
value: /root/agent-harness-work
|
||||||
|
# planner/investigator/implementer stay on the default
|
||||||
|
# (homelab-ornith/ornith:35b, pi's settings.json default). Judge
|
||||||
|
# moves to the separate homelab-reasoning backend (DeepSeek-R1,
|
||||||
|
# its own 2 GPU replicas) so judge calls stop contending with the
|
||||||
|
# other 3 roles for the 2 ornith pods -- an entire role's worth
|
||||||
|
# of traffic moves onto otherwise-idle capacity instead.
|
||||||
|
- name: JUDGE_PROVIDER
|
||||||
|
value: homelab-reasoning
|
||||||
|
- name: JUDGE_MODEL
|
||||||
|
value: reasoning
|
||||||
|
ports:
|
||||||
|
- containerPort: 9090
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "4"
|
||||||
|
memory: 8Gi
|
||||||
|
limits:
|
||||||
|
cpu: "8"
|
||||||
|
memory: 16Gi
|
||||||
|
volumeMounts:
|
||||||
|
- name: pi-config
|
||||||
|
mountPath: /root/.pi/agent/settings.json
|
||||||
|
subPath: settings.json
|
||||||
|
- name: pi-models
|
||||||
|
mountPath: /root/.pi/agent/models.json
|
||||||
|
subPath: models.json
|
||||||
|
- name: pi-skills
|
||||||
|
mountPath: /root/.pi/agent/skills
|
||||||
|
- name: hub-src
|
||||||
|
mountPath: /root/hub.js
|
||||||
|
subPath: hub.js
|
||||||
|
- name: coordinator-src
|
||||||
|
mountPath: /root/coordinator.js
|
||||||
|
subPath: coordinator.js
|
||||||
|
- name: ssh-key
|
||||||
|
mountPath: /root/.ssh/id_forgejo
|
||||||
|
subPath: id_forgejo
|
||||||
|
- name: ssh-config
|
||||||
|
mountPath: /root/.ssh/config
|
||||||
|
subPath: config
|
||||||
|
volumes:
|
||||||
|
- name: pi-config
|
||||||
|
configMap:
|
||||||
|
name: pi-config
|
||||||
|
- name: pi-models
|
||||||
|
secret:
|
||||||
|
secretName: pi-models
|
||||||
|
- name: pi-skills
|
||||||
|
configMap:
|
||||||
|
name: pi-skills
|
||||||
|
items:
|
||||||
|
- key: planner-SKILL.md
|
||||||
|
path: planner/SKILL.md
|
||||||
|
- key: investigator-SKILL.md
|
||||||
|
path: investigator/SKILL.md
|
||||||
|
- key: info-collector-SKILL.md
|
||||||
|
path: info-collector/SKILL.md
|
||||||
|
- key: implementer-SKILL.md
|
||||||
|
path: implementer/SKILL.md
|
||||||
|
- key: judge-SKILL.md
|
||||||
|
path: judge/SKILL.md
|
||||||
|
- key: resolver-SKILL.md
|
||||||
|
path: resolver/SKILL.md
|
||||||
|
- name: hub-src
|
||||||
|
configMap:
|
||||||
|
name: hub-src
|
||||||
|
- name: coordinator-src
|
||||||
|
configMap:
|
||||||
|
name: coordinator-src
|
||||||
|
- name: ssh-key
|
||||||
|
secret:
|
||||||
|
secretName: agent-pod-ssh-key
|
||||||
|
defaultMode: 0600
|
||||||
|
- name: ssh-config
|
||||||
|
configMap:
|
||||||
|
name: agent-pod-ssh-config
|
||||||
@@ -0,0 +1,700 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
data:
|
||||||
|
hub.js: |
|
||||||
|
#!/usr/bin/env node
|
||||||
|
// agent-hub: lives inside the pi container (not a sidecar) so it can spawn
|
||||||
|
// `pi` directly, and control the pod's own tmux server. One persistent
|
||||||
|
// in-cluster service -- POST /run to trigger a single ad-hoc headless agent
|
||||||
|
// run, POST /pipeline to run an ordered list of task phases against a
|
||||||
|
// repo/branch (phases run sequentially, up to PHASE_CONCURRENCY tasks within
|
||||||
|
// a phase run concurrently, each in its own git worktree). Within one task,
|
||||||
|
// planner/investigator/implementer/judge are separate agents in separate
|
||||||
|
// named tmux sessions (task-<id>-<role>, attachable via `kubectl exec -it --
|
||||||
|
// tmux attach -t <name>` while running), coordinating only through what's on
|
||||||
|
// disk in that task's worktree -- not one shared conversation. GET /console
|
||||||
|
// (WebSocket) watches every concurrent headless run live, relaying pi's own
|
||||||
|
// session protocol verbatim (same event shape Claude Code sessions use).
|
||||||
|
const http = require("node:http");
|
||||||
|
const crypto = require("node:crypto");
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
const { spawn } = require("node:child_process");
|
||||||
|
const readline = require("node:readline");
|
||||||
|
const { WebSocketServer } = require("ws");
|
||||||
|
|
||||||
|
const PORT = process.env.HUB_PORT || 9090;
|
||||||
|
const WORK_DIR = process.env.HUB_WORK_DIR || path.join(require("node:os").tmpdir(), "agent-harness-work");
|
||||||
|
|
||||||
|
// Never rely on a bare `pi` on $PATH -- both `pi` and `agent-console` collide
|
||||||
|
// with unrelated tools on this machine (a Rust CLI and a Datadog TUI,
|
||||||
|
// respectively, discovered the hard way this session). Always invoke the
|
||||||
|
// exact pinned @earendil-works/[email protected] installed locally under
|
||||||
|
// .pi-cli/, by explicit path.
|
||||||
|
const PI_BIN =
|
||||||
|
process.env.PI_BIN ||
|
||||||
|
path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
".pi-cli",
|
||||||
|
"node_modules",
|
||||||
|
"@earendil-works",
|
||||||
|
"pi-coding-agent",
|
||||||
|
"dist",
|
||||||
|
"cli.js"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Job-type skills under pi/skills/<name>/SKILL.md (mounted at
|
||||||
|
// ~/.pi/agent/skills/<name>/ in agent-pod). When `agent` matches one of
|
||||||
|
// these, the prompt is forced through pi's `/skill:<name> <args>` mechanism
|
||||||
|
// instead of being sent bare -- see pi's skills.md docs on single-shot skill
|
||||||
|
// loading. `resolver` is never dispatched directly by a caller; only the
|
||||||
|
// pipeline driver invokes it, on stage crashes.
|
||||||
|
const ROLE_SKILLS = new Set([
|
||||||
|
"planner",
|
||||||
|
"investigator",
|
||||||
|
"info-collector",
|
||||||
|
"implementer",
|
||||||
|
"judge",
|
||||||
|
"resolver",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const sessions = new Map(); // id -> {id, agent, status, events, startedAt, endedAt, pipelineId?, stage?}
|
||||||
|
const viewers = new Set(); // WebSocket connections watching /console
|
||||||
|
|
||||||
|
function broadcast(type, session) {
|
||||||
|
const msg = JSON.stringify({ type, session });
|
||||||
|
for (const ws of viewers) {
|
||||||
|
if (ws.readyState === ws.OPEN) ws.send(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startSession(agent, extra = {}) {
|
||||||
|
const id = extra.id || crypto.randomUUID();
|
||||||
|
const session = {
|
||||||
|
...extra,
|
||||||
|
id,
|
||||||
|
agent,
|
||||||
|
status: "running",
|
||||||
|
events: [],
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
sessions.set(id, session);
|
||||||
|
broadcast("start", session);
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEvent(session, rawLine) {
|
||||||
|
const event = JSON.parse(rawLine);
|
||||||
|
session.events.push(event);
|
||||||
|
broadcast("event", session);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
function endSession(session, status) {
|
||||||
|
session.status = status;
|
||||||
|
session.endedAt = new Date().toISOString();
|
||||||
|
broadcast("end", session);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracts the plain-text content of a message_end event, if any -- used to
|
||||||
|
// find the VERDICT:/RESOLUTION: line judge/resolver skills are required to
|
||||||
|
// end their final message with.
|
||||||
|
function textOf(event) {
|
||||||
|
if (event.type !== "message_end" || !event.message || !Array.isArray(event.message.content)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return event.message.content
|
||||||
|
.filter((c) => c.type === "text")
|
||||||
|
.map((c) => c.text)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Core spawn primitive. Spawns `pi -p --mode json <extraArgs> <prompt>`,
|
||||||
|
// relays every line as a session event exactly like before. Returns
|
||||||
|
// { session, done } -- `session` is available synchronously (so an HTTP
|
||||||
|
// handler can respond with its id right away, same as the old runAgent),
|
||||||
|
// `done` is a Promise resolving once the process exits, for callers that
|
||||||
|
// need to wait on a stage (the pipeline driver) rather than fire-and-forget.
|
||||||
|
function spawnPi({ agent, prompt, provider, model, cwd, sessionExtra = {} }) {
|
||||||
|
const session = startSession(agent, sessionExtra);
|
||||||
|
const args = ["-p", "--mode", "json"];
|
||||||
|
if (provider) args.push("--provider", provider);
|
||||||
|
if (model) args.push("--model", model);
|
||||||
|
const finalPrompt = ROLE_SKILLS.has(agent) ? `/skill:${agent} ${prompt}` : prompt;
|
||||||
|
args.push(finalPrompt);
|
||||||
|
|
||||||
|
const child = spawn(PI_BIN, args, {
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
cwd,
|
||||||
|
});
|
||||||
|
const rl = readline.createInterface({ input: child.stdout });
|
||||||
|
let lastText = "";
|
||||||
|
let stderrTail = "";
|
||||||
|
|
||||||
|
rl.on("line", (line) => {
|
||||||
|
if (!line.trim()) return;
|
||||||
|
try {
|
||||||
|
const event = addEvent(session, line);
|
||||||
|
const text = textOf(event);
|
||||||
|
if (text) lastText = text;
|
||||||
|
} catch {
|
||||||
|
// non-JSON stdout noise, ignore
|
||||||
|
}
|
||||||
|
});
|
||||||
|
child.stderr.on("data", (chunk) => {
|
||||||
|
process.stderr.write(chunk);
|
||||||
|
stderrTail = (stderrTail + chunk.toString()).slice(-4000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const done = new Promise((resolve) => {
|
||||||
|
child.on("close", (code) => {
|
||||||
|
endSession(session, code === 0 ? "done" : "error");
|
||||||
|
resolve({ code, session, lastText, stderrTail });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { session, done };
|
||||||
|
}
|
||||||
|
|
||||||
|
function runAgent(agent, prompt, extraArgs = {}) {
|
||||||
|
// Fire-and-forget: caller (the /run handler) doesn't await `done`.
|
||||||
|
return spawnPi({ agent, prompt, ...extraArgs }).session;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseVerdictLine(text, label) {
|
||||||
|
if (!text) return null;
|
||||||
|
const re = new RegExp(`${label}:\\s*(\\w+)`, "i");
|
||||||
|
const m = text.match(re);
|
||||||
|
return m ? m[1].toUpperCase() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deterministic git operations, run directly by hub.js rather than left to
|
||||||
|
// the model -- branch creation and pushing after each task are mechanical,
|
||||||
|
// not judgment calls, and need to happen reliably every time regardless of
|
||||||
|
// what a task's stages did or didn't remember to do.
|
||||||
|
function runCmd(bin, args, cwd) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const child = spawn(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
||||||
|
let out = "";
|
||||||
|
child.stdout.on("data", (c) => (out += c));
|
||||||
|
child.stderr.on("data", (c) => (out += c));
|
||||||
|
child.on("close", (code) => resolve({ code, out: out.trim() }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runGit(cwd, args) {
|
||||||
|
return runCmd("git", args, cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A stage saying "commit" in its prompt is a request, not a guarantee -- seen
|
||||||
|
// in practice: a stage writes a real file and simply never runs `git add`/
|
||||||
|
// `git commit`, leaving it untracked and invisible to every later `git diff`.
|
||||||
|
// Sweep and commit anything left dirty after every stage, deterministically.
|
||||||
|
async function commitPending(cwd, message) {
|
||||||
|
await runGit(cwd, ["add", "-A"]);
|
||||||
|
const status = await runGit(cwd, ["status", "--porcelain"]);
|
||||||
|
if (!status.out) return { committed: false };
|
||||||
|
const commit = await runGit(cwd, ["commit", "-m", message]);
|
||||||
|
return { committed: commit.code === 0, error: commit.code !== 0 ? commit.out : undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invokes the `resolver` skill to diagnose a stuck/crashed stage and decide
|
||||||
|
// RETRY vs ABORT. Shared by both crash-recovery paths below (headless
|
||||||
|
// exit-code failures and interactive sentinel-file timeouts) -- the
|
||||||
|
// diagnostic prompt differs per caller, but "ask resolver, parse the
|
||||||
|
// RESOLUTION: line" is identical either way.
|
||||||
|
async function askResolver(pipelineId, cwd, task, diagnosticPrompt) {
|
||||||
|
const resolverResult = await spawnPi({
|
||||||
|
agent: "resolver",
|
||||||
|
prompt: diagnosticPrompt,
|
||||||
|
cwd,
|
||||||
|
sessionExtra: { pipelineId, stage: "resolver", task },
|
||||||
|
}).done;
|
||||||
|
return parseVerdictLine(resolverResult.lastText, "RESOLUTION");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs one pipeline stage, and if it crashes (nonzero exit -- not a semantic
|
||||||
|
// judge FAIL, which is handled separately), asks the resolver to diagnose
|
||||||
|
// and decide RETRY vs ABORT. Retries the failed stage at most once,
|
||||||
|
// regardless of what resolver recommends a second time -- a hard cap, not
|
||||||
|
// indefinite trust in the model's judgment.
|
||||||
|
async function runStageWithResolver(pipelineId, cwd, stage, prompt, task) {
|
||||||
|
let result = await spawnPi({
|
||||||
|
agent: stage,
|
||||||
|
prompt,
|
||||||
|
cwd,
|
||||||
|
sessionExtra: { pipelineId, stage, task },
|
||||||
|
}).done;
|
||||||
|
if (result.code === 0) return result;
|
||||||
|
|
||||||
|
const resolution = await askResolver(
|
||||||
|
pipelineId,
|
||||||
|
cwd,
|
||||||
|
task,
|
||||||
|
`Stage "${stage}" exited with code ${result.code}. Its stderr tail:\n${result.stderrTail}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (resolution === "RETRY") {
|
||||||
|
result = await spawnPi({
|
||||||
|
agent: stage,
|
||||||
|
prompt,
|
||||||
|
cwd,
|
||||||
|
sessionExtra: { pipelineId, stage, task },
|
||||||
|
}).done;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deterministic tmux operations -- same rationale as runGit: mechanical,
|
||||||
|
// not a judgment call, run directly rather than trusted to a prompt.
|
||||||
|
function runTmux(args) {
|
||||||
|
return runCmd("tmux", args);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tmuxSessionName(task) {
|
||||||
|
return `task-${task.replace(/[^a-zA-Z0-9]/g, "-")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounded-concurrency pool -- runs `worker` over `items`, at most `limit` in
|
||||||
|
// flight at once. No external dep; a plain in-order index cursor shared by
|
||||||
|
// `limit` runner loops.
|
||||||
|
async function runConcurrent(items, limit, worker) {
|
||||||
|
const results = new Array(items.length);
|
||||||
|
let i = 0;
|
||||||
|
async function next() {
|
||||||
|
while (i < items.length) {
|
||||||
|
const idx = i++;
|
||||||
|
results[idx] = await worker(items[idx], idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, next));
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STAGE_TIMEOUT_MS = 10 * 60 * 1000;
|
||||||
|
const NUDGE_TIMEOUT_MS = 5 * 60 * 1000;
|
||||||
|
const POLL_MS = 10 * 1000;
|
||||||
|
|
||||||
|
async function waitForFile(filePath, limitMs) {
|
||||||
|
const start = Date.now();
|
||||||
|
while (!fs.existsSync(filePath)) {
|
||||||
|
if (Date.now() - start > limitMs) return false;
|
||||||
|
await new Promise((r) => setTimeout(r, POLL_MS));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_IMPLEMENT_ATTEMPTS = 5;
|
||||||
|
const MAX_PLAN_REVISIONS = 3;
|
||||||
|
|
||||||
|
// Runs one role as its own fresh interactive pi session in its own named
|
||||||
|
// tmux session -- planner, investigator, implementer, and judge are
|
||||||
|
// separate agents with separate context, not turns in one shared
|
||||||
|
// conversation. They coordinate only through what's on disk in the task's
|
||||||
|
// worktree: PLAN.md, committed code, judge's result file. Each session is
|
||||||
|
// attachable while it runs (kubectl exec -it -- tmux attach -t <name>) and
|
||||||
|
// killed once its sentinel file lands or it's abandoned after resolver
|
||||||
|
// escalation.
|
||||||
|
async function runStage(pipelineId, cwd, task, stageLabel, stagePrompt, sentinelFile) {
|
||||||
|
const sessionName = `${tmuxSessionName(task)}-${stageLabel}`;
|
||||||
|
fs.rmSync(sentinelFile, { force: true });
|
||||||
|
|
||||||
|
const spawned = await runTmux(["new-session", "-d", "-s", sessionName, "-c", cwd, PI_BIN, stagePrompt]);
|
||||||
|
if (spawned.code !== 0) return { ok: false, crashed: true, error: spawned.out, sessionName };
|
||||||
|
|
||||||
|
let ok = await waitForFile(sentinelFile, STAGE_TIMEOUT_MS);
|
||||||
|
if (!ok) {
|
||||||
|
const pane = await runTmux(["capture-pane", "-t", sessionName, "-p", "-S", "-200"]);
|
||||||
|
const resolution = await askResolver(
|
||||||
|
pipelineId,
|
||||||
|
cwd,
|
||||||
|
task,
|
||||||
|
`Task ${task}'s "${stageLabel}" stage hasn't finished after 10 minutes. ` +
|
||||||
|
`Its pane tail:\n${pane.out.slice(-3000)}\n\nDecide: is it still making ` +
|
||||||
|
`real progress and worth nudging to wrap up, or stuck and worth abandoning?`
|
||||||
|
);
|
||||||
|
if (resolution === "RETRY") {
|
||||||
|
await runTmux([
|
||||||
|
"send-keys",
|
||||||
|
"-t",
|
||||||
|
sessionName,
|
||||||
|
`Please wrap up the "${stageLabel}" stage now and touch ${path.basename(sentinelFile)} when done.`,
|
||||||
|
"Enter",
|
||||||
|
]);
|
||||||
|
ok = await waitForFile(sentinelFile, NUDGE_TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await runTmux(["kill-session", "-t", sessionName]);
|
||||||
|
return { ok, sessionName };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs one task in its own git worktree (see runPhase): planner drafts
|
||||||
|
// PLAN.md, investigator confirms it, then implementer and judge go back and
|
||||||
|
// forth -- judge's FAIL rationale lands in .task-result-<task>, which the
|
||||||
|
// next implementer attempt is told to read and address. After
|
||||||
|
// MAX_IMPLEMENT_ATTEMPTS straight fails, planner is brought back in to
|
||||||
|
// judge whether the *plan* itself is wrong, not just the implementation; if
|
||||||
|
// so it revises PLAN.md and the implementer gets a fresh attempt budget
|
||||||
|
// against the new plan. MAX_PLAN_REVISIONS caps this from looping forever
|
||||||
|
// on a task that's genuinely stuck.
|
||||||
|
async function runTaskInteractive(pipelineId, cwd, baseBranch, task, pipelineSession, judgeOnly) {
|
||||||
|
const resultFile = path.join(cwd, `.task-result-${task}`);
|
||||||
|
fs.rmSync(resultFile, { force: true });
|
||||||
|
|
||||||
|
const specHint = `the file under tasks/ starting with "${task}-"`;
|
||||||
|
|
||||||
|
const runRole = async (stageLabel, prompt, sentinel) => {
|
||||||
|
pipelineSession.activeTasks[task] = {
|
||||||
|
stage: stageLabel,
|
||||||
|
sessionName: `${tmuxSessionName(task)}-${stageLabel}`,
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
broadcast("event", pipelineSession);
|
||||||
|
const result = await runStage(pipelineId, cwd, task, stageLabel, prompt, sentinel);
|
||||||
|
await commitPending(cwd, `task: ${task} (${stageLabel})`);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const abandon = (stageLabel, result, attempt) => {
|
||||||
|
delete pipelineSession.activeTasks[task];
|
||||||
|
broadcast("event", pipelineSession);
|
||||||
|
return {
|
||||||
|
task,
|
||||||
|
status: result.crashed ? "spawn-crashed" : "timed-out",
|
||||||
|
error: result.error,
|
||||||
|
stoppedAt: stageLabel,
|
||||||
|
...(attempt !== undefined ? { attempt } : {}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Task's implementation is inherited already-committed (e.g. from a base
|
||||||
|
// branch of prior work) -- try one judge pass against the spec directly
|
||||||
|
// (no PLAN.md exists yet) before paying for a full planner/investigator
|
||||||
|
// redo. PASS ends the task here; FAIL falls through into the normal flow
|
||||||
|
// below, so planner/implementer pick up with the judge's real feedback.
|
||||||
|
if (judgeOnly) {
|
||||||
|
const quick = await runRole(
|
||||||
|
"judge",
|
||||||
|
`Task ${task} may already be implemented on this branch -- check ` +
|
||||||
|
`\`git log --oneline --grep '${task}'\` and the current code directly against its spec ` +
|
||||||
|
`(${specHint})'s acceptance criteria (no PLAN.md exists for this task yet). Write your ` +
|
||||||
|
`verdict to .task-result-${task} as a single "VERDICT: PASS" or "VERDICT: FAIL" line plus ` +
|
||||||
|
`one line of rationale, then run: touch .stage-done-${task}-judge-0`,
|
||||||
|
path.join(cwd, `.stage-done-${task}-judge-0`)
|
||||||
|
);
|
||||||
|
if (!quick.ok) return abandon("judge", quick, 0);
|
||||||
|
|
||||||
|
const quickText = fs.existsSync(resultFile) ? fs.readFileSync(resultFile, "utf8") : "";
|
||||||
|
if (parseVerdictLine(quickText, "VERDICT") === "PASS") {
|
||||||
|
delete pipelineSession.activeTasks[task];
|
||||||
|
broadcast("event", pipelineSession);
|
||||||
|
return { task, status: "done", judgeRationale: quickText, judgeOnlyPass: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await runRole(
|
||||||
|
"planner",
|
||||||
|
`Use the planner skill to draft PLAN.md for task ${task}, reading its spec (${specHint}). Commit PLAN.md, then run: touch .stage-done-${task}-planner`,
|
||||||
|
path.join(cwd, `.stage-done-${task}-planner`)
|
||||||
|
);
|
||||||
|
if (!result.ok) return abandon("planner", result);
|
||||||
|
|
||||||
|
result = await runRole(
|
||||||
|
"investigator",
|
||||||
|
`Use the investigator skill to confirm PLAN.md against real sources, append findings, commit. Then run: touch .stage-done-${task}-investigator`,
|
||||||
|
path.join(cwd, `.stage-done-${task}-investigator`)
|
||||||
|
);
|
||||||
|
if (!result.ok) return abandon("investigator", result);
|
||||||
|
|
||||||
|
let planRevisions = 0;
|
||||||
|
let implementAttempt = 0;
|
||||||
|
let verdict = null;
|
||||||
|
let resultText = "";
|
||||||
|
let justRevisedPlan = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
implementAttempt++;
|
||||||
|
const feedbackHint = fs.existsSync(resultFile)
|
||||||
|
? justRevisedPlan
|
||||||
|
? `.task-result-${task} holds the judge's feedback against the OLD plan, which prompted a plan revision -- ` +
|
||||||
|
`PLAN.md has since changed. Read the current PLAN.md as the source of truth, not the old feedback verbatim.`
|
||||||
|
: `A previous judge review exists at .task-result-${task} -- read it and address every issue it raises.`
|
||||||
|
: "";
|
||||||
|
justRevisedPlan = false;
|
||||||
|
|
||||||
|
result = await runRole(
|
||||||
|
"implementer",
|
||||||
|
`Use the implementer skill to implement what the current PLAN.md specifies (commit as you go). ${feedbackHint} Then run: touch .stage-done-${task}-implementer-${implementAttempt}`,
|
||||||
|
path.join(cwd, `.stage-done-${task}-implementer-${implementAttempt}`)
|
||||||
|
);
|
||||||
|
if (!result.ok) return abandon("implementer", result, implementAttempt);
|
||||||
|
|
||||||
|
result = await runRole(
|
||||||
|
"judge",
|
||||||
|
`Use the judge skill to review the diff against ${baseBranch}...HEAD. Write your verdict to ` +
|
||||||
|
`.task-result-${task} as a single "VERDICT: PASS" or "VERDICT: FAIL" line plus one line of ` +
|
||||||
|
`rationale, then run: touch .stage-done-${task}-judge-${implementAttempt}`,
|
||||||
|
path.join(cwd, `.stage-done-${task}-judge-${implementAttempt}`)
|
||||||
|
);
|
||||||
|
if (!result.ok) return abandon("judge", result, implementAttempt);
|
||||||
|
|
||||||
|
resultText = fs.existsSync(resultFile) ? fs.readFileSync(resultFile, "utf8") : "";
|
||||||
|
verdict = parseVerdictLine(resultText, "VERDICT");
|
||||||
|
if (verdict === "PASS") break;
|
||||||
|
|
||||||
|
if (implementAttempt >= MAX_IMPLEMENT_ATTEMPTS) {
|
||||||
|
if (planRevisions >= MAX_PLAN_REVISIONS) break;
|
||||||
|
planRevisions++;
|
||||||
|
result = await runRole(
|
||||||
|
"planner-revise",
|
||||||
|
`Implementer failed judge review ${MAX_IMPLEMENT_ATTEMPTS} times in a row for task ${task}. Read PLAN.md, ` +
|
||||||
|
`the judge's feedback in .task-result-${task}, and the current diff against ${baseBranch}...HEAD. Decide ` +
|
||||||
|
`whether the plan's approach itself is wrong, not just the implementation -- if so, revise PLAN.md and ` +
|
||||||
|
`commit. If you change the approach, also use the investigator skill to confirm the new approach against ` +
|
||||||
|
`real sources before committing. If the plan is sound, note why in PLAN.md and leave it as-is. Then run: ` +
|
||||||
|
`touch .stage-done-${task}-planner-revise-${planRevisions}`,
|
||||||
|
path.join(cwd, `.stage-done-${task}-planner-revise-${planRevisions}`)
|
||||||
|
);
|
||||||
|
if (!result.ok) return abandon("planner-revise", result, planRevisions);
|
||||||
|
implementAttempt = 0;
|
||||||
|
justRevisedPlan = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete pipelineSession.activeTasks[task];
|
||||||
|
broadcast("event", pipelineSession);
|
||||||
|
|
||||||
|
if (verdict !== "PASS" && planRevisions >= MAX_PLAN_REVISIONS) {
|
||||||
|
return { task, status: "unresolved", judgeRationale: resultText, implementAttempts: implementAttempt, planRevisions };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
task,
|
||||||
|
status: verdict === "PASS" ? "done" : "done-with-concerns",
|
||||||
|
judgeRationale: resultText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const PHASE_CONCURRENCY = 3;
|
||||||
|
|
||||||
|
// Runs one phase (a batch of tasks with no declared dependency on each
|
||||||
|
// other) with up to PHASE_CONCURRENCY tasks in flight at once. Each task
|
||||||
|
// gets its own git worktree off workBranch -- concurrent pi sessions writing
|
||||||
|
// into one shared working tree would corrupt the index; worktrees share the
|
||||||
|
// same object database but give each task an isolated checkout. After a
|
||||||
|
// task's session ends, its branch is merged back into workBranch and pushed,
|
||||||
|
// one merge at a time (git ref updates aren't safe to run concurrently even
|
||||||
|
// though the worktrees themselves are isolated).
|
||||||
|
async function runPhase(pipelineId, cwd, workBranch, phaseTasks, pipelineSession) {
|
||||||
|
// Each entry is either a plain task id, or { id, judgeOnly: true } when
|
||||||
|
// the task's implementation already exists (e.g. inherited from a base
|
||||||
|
// branch) and just needs a real judge pass rather than a full
|
||||||
|
// planner/investigator/implementer redo.
|
||||||
|
const entries = phaseTasks.map((t) => (typeof t === "string" ? { id: t, judgeOnly: false } : t));
|
||||||
|
|
||||||
|
const worktrees = {};
|
||||||
|
for (const entry of entries) {
|
||||||
|
const task = entry.id;
|
||||||
|
const wtDir = path.join(WORK_DIR, pipelineId, `wt-${task.replace(/[^a-zA-Z0-9]/g, "-")}`);
|
||||||
|
const taskBranch = `task/${task}`;
|
||||||
|
const add = await runGit(cwd, ["worktree", "add", "-b", taskBranch, wtDir, workBranch]);
|
||||||
|
if (add.code !== 0) {
|
||||||
|
pipelineSession.taskResults.push({ task, status: "worktree-crashed", error: add.out });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
worktrees[task] = { wtDir, taskBranch };
|
||||||
|
}
|
||||||
|
|
||||||
|
const runnable = entries.filter((e) => worktrees[e.id]);
|
||||||
|
await runConcurrent(runnable, PHASE_CONCURRENCY, async (entry) => {
|
||||||
|
const { wtDir } = worktrees[entry.id];
|
||||||
|
const result = await runTaskInteractive(pipelineId, wtDir, workBranch, entry.id, pipelineSession, entry.judgeOnly);
|
||||||
|
pipelineSession.taskResults.push(result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merge + push sequentially -- ref updates on the shared repo, one at a
|
||||||
|
// time, in the declared task order for this phase.
|
||||||
|
for (const entry of runnable) {
|
||||||
|
const task = entry.id;
|
||||||
|
const { wtDir, taskBranch } = worktrees[task];
|
||||||
|
const result = pipelineSession.taskResults.find((r) => r.task === task);
|
||||||
|
|
||||||
|
const merge = await runGit(cwd, ["merge", "--no-ff", taskBranch, "-m", `merge: ${task}`]);
|
||||||
|
if (merge.code !== 0) {
|
||||||
|
await runGit(cwd, ["merge", "--abort"]);
|
||||||
|
if (result) {
|
||||||
|
result.status = "merge-conflict";
|
||||||
|
result.mergeError = merge.out;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const push = await runGit(cwd, ["push", "-u", "origin", workBranch]);
|
||||||
|
if (result) {
|
||||||
|
result.pushed = push.code === 0;
|
||||||
|
if (!result.pushed) result.pushError = push.out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await runGit(cwd, ["worktree", "remove", wtDir, "--force"]);
|
||||||
|
await runGit(cwd, ["branch", "-D", taskBranch]);
|
||||||
|
broadcast("event", pipelineSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tasks: array of phases, each phase an array of task ids with no declared
|
||||||
|
// dependency on each other (e.g. [["T0.1","T0.2"], ["T1.1","T1.2","T1.3"]]) --
|
||||||
|
// caller's responsibility to supply real phase grouping (see tasks/INDEX.md;
|
||||||
|
// filename/numeric sort does NOT match execution order on boards like this).
|
||||||
|
// A flat array of ids is also accepted and treated as one single phase.
|
||||||
|
// Phases run strictly sequentially (a phase boundary is a real dependency
|
||||||
|
// gate); tasks within a phase run concurrently, each in its own worktree --
|
||||||
|
// see runPhase.
|
||||||
|
function runPipeline({ pipelineId, repo, baseBranch, tasks, branchName }) {
|
||||||
|
const cwd = path.join(WORK_DIR, pipelineId);
|
||||||
|
fs.mkdirSync(cwd, { recursive: true });
|
||||||
|
|
||||||
|
const phases = Array.isArray(tasks[0]) ? tasks : [tasks];
|
||||||
|
|
||||||
|
const pipelineSession = startSession("pipeline", {
|
||||||
|
id: pipelineId,
|
||||||
|
pipelineId,
|
||||||
|
stage: "pipeline",
|
||||||
|
taskResults: [],
|
||||||
|
activeTasks: {},
|
||||||
|
totalTasks: phases.flat().length,
|
||||||
|
});
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const clone = await runStageWithResolver(
|
||||||
|
pipelineId,
|
||||||
|
cwd,
|
||||||
|
"planner",
|
||||||
|
`Run exactly this command, verbatim, no variation: git clone --branch ${baseBranch} ${repo} . -- the trailing dot is required, it clones directly into the current directory instead of creating a subdirectory. Do not cd anywhere first or after. Do nothing else.`,
|
||||||
|
"clone"
|
||||||
|
);
|
||||||
|
if (clone.code !== 0) return endSession(pipelineSession, "clone-crashed");
|
||||||
|
if (!fs.existsSync(path.join(cwd, ".git"))) {
|
||||||
|
// The model deciding to `cd` elsewhere before cloning (instead of
|
||||||
|
// cloning into the assigned cwd) is a real failure mode seen in
|
||||||
|
// practice, not a hypothetical -- exit code 0 doesn't mean the clone
|
||||||
|
// landed where every later stage's cwd assumes it did.
|
||||||
|
return endSession(pipelineSession, "clone-missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dedicated branch, never main -- and pushed after every single task
|
||||||
|
// (not just at the end) so a pod restart mid-run loses at most the
|
||||||
|
// in-progress task's work, not everything since the start.
|
||||||
|
const workBranch = branchName || `agent-run/${pipelineId}`;
|
||||||
|
const branchResult = await runGit(cwd, ["checkout", "-b", workBranch]);
|
||||||
|
if (branchResult.code !== 0) {
|
||||||
|
pipelineSession.gitError = branchResult.out;
|
||||||
|
return endSession(pipelineSession, "branch-crashed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seen in practice: a task manually downloads a dependency tarball
|
||||||
|
// (crates.io registry access isn't guaranteed from every sandboxed
|
||||||
|
// checkout) and it lands at repo root, outside whatever .gitignore
|
||||||
|
// already covers -- then git add -A (ours or the model's own) commits
|
||||||
|
// it. Append broad build-artifact/archive patterns before any task
|
||||||
|
// runs, so it's excluded regardless of who stages files later.
|
||||||
|
const gitignoreAdditions = [
|
||||||
|
"",
|
||||||
|
"# agent-harness: build artifacts and vendored archives never belong in source control",
|
||||||
|
"*.tar.gz",
|
||||||
|
"*.tgz",
|
||||||
|
"*.crate",
|
||||||
|
"*.zip",
|
||||||
|
"*.bin",
|
||||||
|
"*.whl",
|
||||||
|
"vendor/",
|
||||||
|
"node_modules/",
|
||||||
|
"",
|
||||||
|
"# agent-harness: task completion sentinel files, harness bookkeeping only",
|
||||||
|
".task-result-*",
|
||||||
|
".stage-done-*",
|
||||||
|
].join("\n");
|
||||||
|
fs.appendFileSync(path.join(cwd, ".gitignore"), gitignoreAdditions + "\n");
|
||||||
|
await runGit(cwd, ["add", ".gitignore"]);
|
||||||
|
await runGit(cwd, ["commit", "-m", "chore: broaden .gitignore for agent-run artifacts"]);
|
||||||
|
|
||||||
|
for (const phaseTasks of phases) {
|
||||||
|
await runPhase(pipelineId, cwd, workBranch, phaseTasks, pipelineSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
const crashed = pipelineSession.taskResults.filter((r) => r.status.endsWith("-crashed"));
|
||||||
|
endSession(pipelineSession, crashed.length > 0 ? "completed-with-crashes" : "completed");
|
||||||
|
})();
|
||||||
|
|
||||||
|
return pipelineSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const url = new URL(req.url, "http://localhost");
|
||||||
|
|
||||||
|
if (url.pathname === "/healthz") {
|
||||||
|
res.writeHead(200).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === "/sessions" && req.method === "GET") {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify([...sessions.values()]));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === "/run" && req.method === "POST") {
|
||||||
|
let body = "";
|
||||||
|
req.on("data", (chunk) => (body += chunk));
|
||||||
|
req.on("end", () => {
|
||||||
|
try {
|
||||||
|
const { agent, prompt, provider, model } = JSON.parse(body);
|
||||||
|
if (!agent || !prompt) throw new Error("agent and prompt are required");
|
||||||
|
const session = runAgent(agent, prompt, { provider, model });
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ id: session.id }));
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(400, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ error: err.message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === "/pipeline" && req.method === "POST") {
|
||||||
|
let body = "";
|
||||||
|
req.on("data", (chunk) => (body += chunk));
|
||||||
|
req.on("end", () => {
|
||||||
|
try {
|
||||||
|
const { repo, baseBranch, tasks, branchName } = JSON.parse(body);
|
||||||
|
if (!repo || !baseBranch || !Array.isArray(tasks) || tasks.length === 0) {
|
||||||
|
throw new Error("repo, baseBranch, and a non-empty tasks array are required");
|
||||||
|
}
|
||||||
|
const pipelineId = crypto.randomUUID();
|
||||||
|
runPipeline({ pipelineId, repo, baseBranch, tasks, branchName });
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ id: pipelineId }));
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(400, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ error: err.message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404).end();
|
||||||
|
});
|
||||||
|
|
||||||
|
const wss = new WebSocketServer({ server, path: "/console" });
|
||||||
|
wss.on("connection", (ws) => {
|
||||||
|
for (const session of sessions.values()) {
|
||||||
|
ws.send(JSON.stringify({ type: "snapshot", session }));
|
||||||
|
}
|
||||||
|
viewers.add(ws);
|
||||||
|
ws.on("close", () => viewers.delete(ws));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, () => console.log(`agent-hub listening on :${PORT}`));
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: hub-src
|
||||||
|
namespace: agent-pod
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: agent-hub
|
||||||
|
namespace: agent-pod
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: agent-pod
|
||||||
|
ports:
|
||||||
|
- port: 9090
|
||||||
|
targetPort: 9090
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: agent-pod
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- configmap.yaml
|
||||||
|
- hub-configmap.yaml
|
||||||
|
- coordinator-configmap.yaml
|
||||||
|
- pi-skills-configmap.yaml
|
||||||
|
- ssh-configmap.yaml
|
||||||
|
- hub-service.yaml
|
||||||
|
- console-ingress.yaml
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
data:
|
||||||
|
implementer-SKILL.md: |
|
||||||
|
---
|
||||||
|
name: implementer
|
||||||
|
description: Turns a confirmed PLAN.md into real code changes in the current checkout, committing incrementally. Use as the implementation stage of a spec-to-push pipeline, after planner and investigator have run.
|
||||||
|
allowed-tools: Read Grep Find Ls Write Edit Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
Execute the already-agreed plan; don't re-litigate it. `PLAN.md` (plus any `## Investigation` flags) is the source of truth for *what*; use judgment only for *how*, within the codebase's existing conventions.
|
||||||
|
|
||||||
|
- Read `PLAN.md` top to bottom. Treat flagged/unconfirmed steps conservatively (safer, more literal reading; note it in the commit). Work steps in order. Commit after each meaningful step (`git add -A && git commit -m "..."`), not one giant commit — the judge stage needs real diff history.
|
||||||
|
- Push only if the task explicitly asks for it.
|
||||||
|
|
||||||
|
Match existing style. Don't refactor or "improve" code the plan didn't ask you to touch.
|
||||||
|
|
||||||
|
**UI/frontend changes:** don't trust that the code compiles as proof it works. Start the app (or its dev server) and use `npx playwright` via Bash to actually load the page and look — screenshot the affected view before and after your change, and click through the golden path the plan describes. `npx playwright screenshot <url> out.png` for a quick visual check; for interaction (clicks, form fills, navigation), write a small throwaway script under a scratch path (e.g. `/tmp/`, never committed) using `playwright` the library, run it with `node`, then delete it. This doesn't apply to non-UI work (a Rust library, a CLI, a backend-only change) — use judgment.
|
||||||
|
|
||||||
|
**Hard rules:**
|
||||||
|
- Follow DRY and SOLID. Don't duplicate logic that already exists elsewhere in the codebase you're touching — reuse or extract instead. Keep each unit responsible for one thing.
|
||||||
|
- Never commit anything that doesn't belong in source control: build artifacts, downloaded/vendored dependencies, secrets, scratch/debug files. `.gitignore` already blocks common patterns; if you create something outside those patterns, delete it before committing rather than relying on `.gitignore` to catch it.
|
||||||
|
|
||||||
|
**Never vendor a dependency by downloading/extracting it into the repo.** Use the language's real package manager (`cargo add`, `npm install`, etc.) so the dependency is declared in the manifest and lockfile, not a tarball or extracted source tree sitting in the checkout. If the package manager can't reach its registry from here, say so in your commit message rather than working around it — a later commit sweep (`git add -A`) commits whatever's in the checkout, including anything downloaded for a workaround, even if you never intended to keep it.
|
||||||
|
|
||||||
|
info-collector-SKILL.md: |
|
||||||
|
---
|
||||||
|
name: info-collector
|
||||||
|
description: Gathers and summarizes information on a topic from the web without judging or confirming any particular approach. Use standalone when you need raw research/context on a subject, not a verdict on a specific plan (that's the investigator skill).
|
||||||
|
allowed-tools: Read Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
**Persona:** You are a research assistant. Your job is to gather relevant information on a topic and summarize it neutrally — you are not asked to approve, reject, or recommend anything, just to collect and organize what's out there.
|
||||||
|
|
||||||
|
**Thinking mode:** Medium — breadth of coverage matters more than deep verification here (that's the investigator skill's job).
|
||||||
|
|
||||||
|
**Modes:**
|
||||||
|
|
||||||
|
- **Collect mode** (default) — search the web (`curl` against `https://api.search.brave.com/res/v1/web/search`, header `X-Subscription-Token: $BRAVE_API_KEY`, `--data-urlencode "q=<query>"`) with varied queries to cover the topic from multiple angles, then produce a structured summary: topic areas found, key facts, and links to sources for each. Do not editorialize about which approach is "right" — that's out of scope for this skill.
|
||||||
|
- If asked to write the summary to a file, write it and report the path; otherwise return it directly in your response.
|
||||||
|
investigator-SKILL.md: |
|
||||||
|
---
|
||||||
|
name: investigator
|
||||||
|
description: Reads an existing PLAN.md and confirms its approach against real, current sources via web search, appending findings and flags. Use to sanity-check a plan before implementation, or standalone to verify a claimed approach is actually correct.
|
||||||
|
allowed-tools: Read Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
Check whether the plan's claims about the real world are actually true right now. Cite sources; don't assert without one.
|
||||||
|
|
||||||
|
- Read `PLAN.md` and the spec docs on disk. For each claim depending on external facts (a library's current API, a service's behavior), search the web (`curl` against `https://api.search.brave.com/res/v1/web/search`, header `X-Subscription-Token: $BRAVE_API_KEY`) to confirm or refute it. Append a `## Investigation` section to `PLAN.md`: each claim, its source(s), PASS/FLAG. Commit: `git add PLAN.md && git commit -m "investigate: confirm plan against sources"`.
|
||||||
|
- No `PLAN.md`? Just answer the question asked, citing sources.
|
||||||
|
|
||||||
|
Flag unconfirmed/contradicted claims rather than silently fixing them — that decision belongs to whoever reads the flag next.
|
||||||
|
|
||||||
|
**Never download anything into the repo checkout.** Need to inspect a dependency's real source/docs? Fetch into `/tmp/`, not the repo tree — a later `git add -A` sweep commits whatever's sitting in the checkout, staged or not.
|
||||||
|
judge-SKILL.md: |
|
||||||
|
---
|
||||||
|
name: judge
|
||||||
|
description: LLM-as-judge. Reviews a git diff against PLAN.md and the original spec, and returns a PASS/FAIL verdict with rationale. Use as the final review/report stage of a spec-to-push pipeline (the implementer stage already pushed; this reports on what shipped), or standalone to review any diff against stated criteria.
|
||||||
|
allowed-tools: Read Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
Independent reviewer. Judge whether the implementation satisfies the plan and spec, on the evidence in front of you — not on how confident the commit messages sound. Don't rubber-stamp.
|
||||||
|
|
||||||
|
- Run `git diff <base-branch>...HEAD` to see exactly what changed. Compare against `PLAN.md`'s steps and the spec docs. Does every step have a corresponding change? Does the diff contradict any investigator flag? Anything obviously broken on inspection?
|
||||||
|
- **UI/frontend changes:** a diff that reads correctly can still render broken. Start the app and use `npx playwright` via Bash to actually look — screenshot the affected view, click through the golden path the plan/spec describes. FAIL on a visual defect the diff alone wouldn't show (broken layout, a control that doesn't do what its code claims, a state the plan promised that never renders). Doesn't apply to non-UI work — use judgment.
|
||||||
|
- FAIL on DRY/SOLID violations (duplicated logic that should reuse existing code, mixed-responsibility units) and on anything committed that doesn't belong in source control (build artifacts, vendored dependencies, secrets, scratch files) — name the specific file/lines in your rationale.
|
||||||
|
- No `PLAN.md`/base given? Review whatever diff/criteria are in the task directly.
|
||||||
|
|
||||||
|
You MUST end your final message with a literal verdict line, exactly one of:
|
||||||
|
|
||||||
|
```
|
||||||
|
VERDICT: PASS
|
||||||
|
```
|
||||||
|
```
|
||||||
|
VERDICT: FAIL
|
||||||
|
```
|
||||||
|
|
||||||
|
followed by your rationale. The pipeline driver parses this exact line mechanically to record the outcome — omitting it or rephrasing it breaks the pipeline.
|
||||||
|
|
||||||
|
planner-SKILL.md: |
|
||||||
|
---
|
||||||
|
name: planner
|
||||||
|
description: Clones a target repo/branch, reads its markdown specs, and writes a verifiable step-by-step implementation plan (PLAN.md). Use as the first stage of a spec-to-PR pipeline, or standalone when asked to plan out a task before implementing it.
|
||||||
|
allowed-tools: Read Grep Find Ls Write Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
Plan a concrete, verifiable implementation. Don't implement — that's `implementer`'s job, after `investigator` confirms.
|
||||||
|
|
||||||
|
- If cwd is empty: `git clone` the given repo/branch first (only at pipeline start).
|
||||||
|
- Read every markdown spec file for the task. Decompose into a numbered list of concrete steps, each naming the files/areas it touches and how to verify it's done. State assumptions explicitly; if ambiguous, pick the literal reading and note the ambiguity.
|
||||||
|
- Write the plan to exactly `./PLAN.md` in the repo root — not `tasks/PLAN.md`, not `plans/<name>.md`, not any other name or location. Every later stage looks for the plan at that exact path. Commit: `git add PLAN.md && git commit -m "plan: <summary>"`.
|
||||||
|
|
||||||
|
Don't touch any other file.
|
||||||
|
resolver-SKILL.md: |
|
||||||
|
---
|
||||||
|
name: resolver
|
||||||
|
description: Diagnoses why a pipeline stage crashed (nonzero exit, not a semantic pass/fail) and decides whether it's safe to retry. Invoked by the pipeline driver when a planner/investigator/implementer/judge stage process fails to run to completion.
|
||||||
|
allowed-tools: Read Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
**Persona:** You are an incident triager, not a fixer. A pipeline stage stopped running — your job is to look at what's on disk and what the failed stage's own output said, figure out why, and decide whether re-running that stage is likely to succeed or would just fail the same way again.
|
||||||
|
|
||||||
|
**Thinking mode:** Medium — this is triage (root cause + retry/no-retry judgment), not deep design work.
|
||||||
|
|
||||||
|
**Modes:**
|
||||||
|
|
||||||
|
- **Triage mode** (default) — read the failed stage's name and its last stdout/stderr tail (given in the task). Check the working directory's current state (`git status`, `git log -1`) to see what, if anything, that stage managed to do before stopping. Distinguish transient causes (network blip, a flaky command, an interrupted git operation left in a bad-but-fixable state) from structural ones (the plan itself is broken, a required tool/credential is missing, the repo is in a state no retry will fix).
|
||||||
|
|
||||||
|
You MUST end your final message with a literal resolution line, exactly one of:
|
||||||
|
|
||||||
|
```
|
||||||
|
RESOLUTION: RETRY
|
||||||
|
```
|
||||||
|
```
|
||||||
|
RESOLUTION: ABORT
|
||||||
|
```
|
||||||
|
|
||||||
|
followed by your rationale. The pipeline driver parses this exact line mechanically and retries the failed stage **at most once** regardless of what you recommend a second time — don't assume unlimited retries. If the working directory is left in a broken state that a retry needs cleaned up first (e.g. a half-finished `git` operation), say so and do that cleanup yourself (via `bash`) before recommending `RETRY`.
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: pi-skills
|
||||||
|
namespace: agent-pod
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: agent-pod-ssh-config
|
||||||
|
namespace: agent-pod
|
||||||
|
data:
|
||||||
|
config: |
|
||||||
|
Host git.riotpiao.com
|
||||||
|
IdentityFile /root/.ssh/id_forgejo
|
||||||
|
Port 2222
|
||||||
|
User git
|
||||||
|
StrictHostKeyChecking accept-new
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# API Auth Layer — Authentik service account + Kong JWT (model invoke)
|
||||||
|
|
||||||
|
Protect the model API (`api.riotpiao.com/*`, Kong OSS 3.9) so only an Authentik
|
||||||
|
service account holding a valid **client_credentials** JWT can invoke the KServe
|
||||||
|
models. "Invoke role" = **possession of a JWT from the dedicated model-invoke
|
||||||
|
OAuth2 provider** (only the service account can obtain one).
|
||||||
|
|
||||||
|
## Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
service account ── client_credentials ──▶ Authentik token endpoint
|
||||||
|
(client_id + secret) https://authentik.riotpiao.com/application/o/token/
|
||||||
|
│
|
||||||
|
▼ RS256 JWT (iss = https://authentik.riotpiao.com/application/o/model-invoke/)
|
||||||
|
client ── Authorization: Bearer <jwt> ──▶ Kong (api.riotpiao.com/*)
|
||||||
|
jwt plugin: verify RS256 sig via Authentik JWKS,
|
||||||
|
check iss/exp → map to KongConsumer → allow
|
||||||
|
▼
|
||||||
|
KServe model (reasoning / ornith / ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Kong OSS has no enterprise `openid-connect` plugin, so we use the built-in
|
||||||
|
**`jwt`** plugin: it validates an RS256 signature against a public key we pin on
|
||||||
|
a KongConsumer, keyed by the token's `iss`.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### 1. Authentik (k8s/infra/iam/scripts/authentik-provision.py)
|
||||||
|
- New **service account** user `model-invoker` (type `service_account`, no
|
||||||
|
password; Authentik issues an app-password/token for M2M).
|
||||||
|
- New **OAuth2 provider + application** `model-invoke`:
|
||||||
|
- `client_type: confidential`, `grant_types: ["client_credentials"]`
|
||||||
|
- signing key = existing RS256 keypair (same as other providers)
|
||||||
|
- mappings: `openid` (+ optionally a static `invoke` scope) — no user scopes
|
||||||
|
needed for M2M.
|
||||||
|
- Client secret written to k8s Secret `api/model-invoke-oidc`
|
||||||
|
(keys `client-id`, `client-secret`), labelled for whoever consumes it.
|
||||||
|
- Bind the service account so it (and only it) can use the provider.
|
||||||
|
|
||||||
|
### 2. Kong (k8s/apps/api/, new file `model-auth.yaml`)
|
||||||
|
- **KongConsumer** `model-invoker` (ns api).
|
||||||
|
- **`jwt` credential** on that consumer (a Secret of type
|
||||||
|
`konghq.com/v1/credential`):
|
||||||
|
- `algorithm: RS256`
|
||||||
|
- `key` = the token `iss` → `https://authentik.riotpiao.com/application/o/model-invoke/`
|
||||||
|
- `rsa_public_key` = the PEM public key of Authentik's `model-invoke` signing
|
||||||
|
cert (fetched from Authentik JWKS / cert, stored in git or ksops).
|
||||||
|
- **KongPlugin** `jwt-auth` (`plugin: jwt`, `config.claims_to_verify: [exp]`).
|
||||||
|
|
||||||
|
### 3. Wire onto model routes (k8s/apps/api/llm-routes.yaml)
|
||||||
|
- Add `jwt-auth` to each model Ingress's `konghq.com/plugins` annotation
|
||||||
|
(currently e.g. `llm-rewrite-reasoning`) → becomes
|
||||||
|
`llm-rewrite-reasoning,jwt-auth`.
|
||||||
|
- Leave `/models` list route open OR protect too (decision).
|
||||||
|
|
||||||
|
## Client usage (after build)
|
||||||
|
```bash
|
||||||
|
TOKEN=$(curl -s https://authentik.riotpiao.com/application/o/token/ \
|
||||||
|
-d grant_type=client_credentials \
|
||||||
|
-d client_id=model-invoke \
|
||||||
|
-d client_secret=<secret> \
|
||||||
|
-d scope=openid | jq -r .access_token)
|
||||||
|
|
||||||
|
curl https://api.riotpiao.com/v1/chat/completions \
|
||||||
|
-H "Authorization: Bearer $TOKEN" -d '{...}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test plan
|
||||||
|
1. No token → Kong returns 401.
|
||||||
|
2. Valid client_credentials token → 200, model responds.
|
||||||
|
3. Expired/garbage token → 401.
|
||||||
|
4. Confirm the `/models` route behaviour matches the decision.
|
||||||
|
|
||||||
|
## Open items / risks
|
||||||
|
- Authentik `client_credentials` for a *service account* may require an
|
||||||
|
**app-password / JWT-assertion** flow rather than plain client_secret POST —
|
||||||
|
verify Authentik 2026.x M2M exactly (client_credentials with client_secret vs
|
||||||
|
the SA token). Adjust step 1 accordingly before wiring Kong.
|
||||||
|
- Pinning `rsa_public_key`: Authentik key rotation would break it — document a
|
||||||
|
rotation runbook, or have the provision script re-export the cert PEM into the
|
||||||
|
Kong credential on each run (keeps them in sync, same idea as ksops secrets).
|
||||||
|
- Kong `jwt` maps token→consumer by the `iss`=`key` match; ensure the provider's
|
||||||
|
issuer is stable.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Edge route for the API gateway.
|
||||||
|
#
|
||||||
|
# Lives here rather than in the central k8s/bootstrap/ingress/ingress.yaml
|
||||||
|
# because that Application syncs in wave 1, before namespace `api` exists.
|
||||||
|
#
|
||||||
|
# nginx terminates TLS with the wildcard *.riotpiao.com cert (served as its
|
||||||
|
# default-ssl-certificate, so no per-rule `tls:` block is needed) and forwards
|
||||||
|
# plain HTTP to kong-proxy. Kong then does the real routing, from Ingresses
|
||||||
|
# carrying `ingressClassName: kong`.
|
||||||
|
#
|
||||||
|
# Catch-all `/` on purpose: everything under this host belongs to Kong. Listing
|
||||||
|
# per-API paths here would duplicate Kong's routing table inside nginx, and the
|
||||||
|
# two copies would drift.
|
||||||
|
#
|
||||||
|
# In-cluster callers should prefer http://kong-proxy.api.svc.cluster.local
|
||||||
|
# directly. Resolving api.riotpiao.com sends them out to nginx and back in,
|
||||||
|
# which is a pointless hairpin unless they need TLS or the public hostname.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: api
|
||||||
|
namespace: api
|
||||||
|
annotations:
|
||||||
|
# An API gateway carries streaming responses (SSE, gRPC-web, LLM token
|
||||||
|
# streams). nginx's 60s default read timeout and its response buffering
|
||||||
|
# would truncate or stall those.
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-buffering: "off"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: api.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: kong-proxy
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Cluster-wide Kong Prometheus plugin -- `global: "true"` label makes the
|
||||||
|
# ingress controller apply it to every route on this Kong instance, so all
|
||||||
|
# five LLM routes (ornith/reasoning/qwen/embeddings/rerank) get RED metrics
|
||||||
|
# without touching llm-routes.yaml. Scraped via kong-values.yaml's
|
||||||
|
# serviceMonitor (status listener, already on by chart default at :8100).
|
||||||
|
apiVersion: configuration.konghq.com/v1
|
||||||
|
kind: KongClusterPlugin
|
||||||
|
metadata:
|
||||||
|
name: prometheus
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: kong
|
||||||
|
labels:
|
||||||
|
global: "true"
|
||||||
|
plugin: prometheus
|
||||||
|
config:
|
||||||
|
status_code_metrics: true
|
||||||
|
latency_metrics: true
|
||||||
|
bandwidth_metrics: true
|
||||||
|
upstream_health_metrics: true
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# Kong Gateway — cluster-internal API gateway (namespace `api`).
|
||||||
|
#
|
||||||
|
# Chart: kong/kong 3.4.1 (appVersion 3.9). Only overrides are listed; every key
|
||||||
|
# here was checked against `helm show values kong/kong --version 3.4.1`, because
|
||||||
|
# Helm silently ignores unknown keys — a typo is a no-op, not an error.
|
||||||
|
#
|
||||||
|
# ── Topology ────────────────────────────────────────────────────────────────
|
||||||
|
# external: client -> nginx (TLS, wildcard *.riotpiao.com) -> kong-proxy:80
|
||||||
|
# internal: pod -> kong-proxy.api.svc.cluster.local:80
|
||||||
|
#
|
||||||
|
# nginx stays the single edge and the only LoadBalancer (192.168.1.160). Kong is
|
||||||
|
# the policy/routing layer behind it, so it needs no LB IP and no TLS of its own
|
||||||
|
# — hence ClusterIP and proxy.tls disabled. Giving Kong its own IP from
|
||||||
|
# homelab-pool would mean duplicating cert-manager wiring and diverging from the
|
||||||
|
# CoreDNS convention that sends every *.riotpiao.com host to nginx.
|
||||||
|
#
|
||||||
|
# ── Routing model ───────────────────────────────────────────────────────────
|
||||||
|
# Consumers publish an Ingress with `ingressClassName: kong`; the controller
|
||||||
|
# turns it into a Kong route. `nginx` remains the default IngressClass, so this
|
||||||
|
# is strictly opt-in and no existing Ingress changes behaviour.
|
||||||
|
|
||||||
|
# Without this the release name is prefixed onto everything (`kong-kong-proxy`).
|
||||||
|
# Pinning it keeps the Service name stable and independent of the release name,
|
||||||
|
# which matters because the nginx Ingress in k8s/bootstrap/ingress/ingress.yaml
|
||||||
|
# references it by name.
|
||||||
|
fullnameOverride: kong
|
||||||
|
|
||||||
|
# Two replicas so a node drain or rollout doesn't take the gateway down. Kong is
|
||||||
|
# stateless in DB-less mode, so replicas are pure redundancy.
|
||||||
|
replicaCount: 2
|
||||||
|
|
||||||
|
# Opt in to the `llm-serving-default-deny` NetworkPolicy, which admits port 8080
|
||||||
|
# only from pods carrying this label. That policy is a compensating control, not
|
||||||
|
# hygiene: vLLM v0.11.0 is frozen on Volta and will never receive patches for
|
||||||
|
# several remote/unauthenticated advisories, so it must not be broadly reachable.
|
||||||
|
#
|
||||||
|
# Without this label Cilium DROPS the packets rather than refusing them, so the
|
||||||
|
# symptom is a request that hangs until the client's timeout — not a connection
|
||||||
|
# error. /v1/models still worked while this was missing, because
|
||||||
|
# request-termination answers inside Kong and never touches an upstream.
|
||||||
|
podLabels:
|
||||||
|
llm-client: "true"
|
||||||
|
|
||||||
|
env:
|
||||||
|
# DB-less. Config comes from Kubernetes objects via the ingress controller, so
|
||||||
|
# git stays the source of truth. A Postgres-backed Kong would put live routing
|
||||||
|
# config in a database mutated through the Admin API — state outside git, plus
|
||||||
|
# migration Jobs on every upgrade.
|
||||||
|
database: "off"
|
||||||
|
# `nginx_proxy_<directive>` injects a directive into the proxy location block;
|
||||||
|
# this renders `proxy_buffering off;`.
|
||||||
|
#
|
||||||
|
# Required for LLM streaming. With buffering on (the default) nginx accumulates
|
||||||
|
# the upstream response before forwarding, so an SSE stream from
|
||||||
|
# `"stream": true` arrives in lumps or stalls until the generation finishes —
|
||||||
|
# which defeats the point of streaming. The matching setting is already on the
|
||||||
|
# nginx Ingress in ingress.yaml; both hops have to be unbuffered or the
|
||||||
|
# buffered one dominates.
|
||||||
|
nginx_proxy_proxy_buffering: "off"
|
||||||
|
# Any plugin that rewrites the request body — request-transformer on the
|
||||||
|
# llm-chat-* routes — reads it through `kong.request.get_body()`, and that
|
||||||
|
# returns nothing once nginx has spilled the body past
|
||||||
|
# client_body_buffer_size into a temp file. The plugin then re-serializes a
|
||||||
|
# body with no `messages`, and the upstream answers
|
||||||
|
# HTTP 400 {"error":{"message":"[] is too short - 'messages'"}}
|
||||||
|
# Measured on /v1/ornith/chat/completions: 10588 B -> 200, 11088 B -> 400.
|
||||||
|
# An agent request carrying tool schemas clears that in one turn, so the
|
||||||
|
# buffer has to hold a whole conversation, not a chat message.
|
||||||
|
nginx_http_client_body_buffer_size: "16m"
|
||||||
|
nginx_http_client_max_body_size: "16m"
|
||||||
|
|
||||||
|
ingressController:
|
||||||
|
enabled: true
|
||||||
|
ingressClass: kong
|
||||||
|
# The chart's ingress-class template is gated on
|
||||||
|
# `.Capabilities.APIVersions.Has "networking.k8s.io/v1/IngressClass"`, so a
|
||||||
|
# bare `helm template` renders nothing. ArgoCD passes --api-versions from the
|
||||||
|
# live cluster, so it does render there — verify `kubectl get ingressclass
|
||||||
|
# kong` after the first sync rather than assuming it.
|
||||||
|
createIngressClass: true
|
||||||
|
# Deliberately empty: setting is-default-class here would hijack every Ingress
|
||||||
|
# in the cluster that omits ingressClassName. nginx keeps that role.
|
||||||
|
ingressClassAnnotations: {}
|
||||||
|
|
||||||
|
proxy:
|
||||||
|
enabled: true
|
||||||
|
# Chart default is LoadBalancer, which would claim an IP from homelab-pool.
|
||||||
|
type: ClusterIP
|
||||||
|
http:
|
||||||
|
enabled: true
|
||||||
|
servicePort: 80
|
||||||
|
containerPort: 8000
|
||||||
|
# nginx already terminated TLS; a second handshake to the same cluster buys
|
||||||
|
# nothing and would need Kong to hold its own certificate.
|
||||||
|
tls:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# No Service for the Admin API. The controller reaches it over localhost inside
|
||||||
|
# the pod, so exposing it would only create an unauthenticated write path to the
|
||||||
|
# gateway's entire configuration.
|
||||||
|
admin:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# Kong Manager UI — chart default is `enabled: true` with type NodePort, which
|
||||||
|
# would open a port on every node. Not wanted.
|
||||||
|
manager:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 200m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: "2"
|
||||||
|
memory: 1Gi
|
||||||
|
|
||||||
|
podDisruptionBudget:
|
||||||
|
enabled: true
|
||||||
|
minAvailable: 1
|
||||||
|
|
||||||
|
# Status listener (metrics/health) is on by default at :8100 (chart default,
|
||||||
|
# verified via `helm show values`). This just wires the ServiceMonitor the
|
||||||
|
# chart already knows how to generate for it, so kong_http_requests_total /
|
||||||
|
# kong_latency_* / kong_bandwidth_bytes land in Prometheus. Paired with the
|
||||||
|
# cluster-wide `prometheus` KongClusterPlugin in kong-metrics.yaml.
|
||||||
|
serviceMonitor:
|
||||||
|
enabled: true
|
||||||
|
labels:
|
||||||
|
release: kube-prometheus-stack
|
||||||
|
|
||||||
|
# Spread the two replicas across nodes; `ScheduleAnyway` so a single-node
|
||||||
|
# situation degrades to co-location instead of leaving a pod Pending.
|
||||||
|
topologySpreadConstraints:
|
||||||
|
- maxSkew: 1
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
whenUnsatisfiable: ScheduleAnyway
|
||||||
|
labelSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: kong
|
||||||
|
app.kubernetes.io/instance: kong
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
# Explicit allowlist so kong-values.yaml in this directory is NOT treated as a
|
||||||
|
# manifest — it is Helm input consumed by the chart source of the `kong`
|
||||||
|
# Application, not a Kubernetes object. Anything new added here must be listed
|
||||||
|
# or it is silently dropped with no error and no drift shown.
|
||||||
|
resources:
|
||||||
|
- ingress.yaml
|
||||||
|
- kong-metrics.yaml
|
||||||
|
- llm-routes.yaml
|
||||||
|
- model-auth.yaml
|
||||||
|
# No top-level `namespace:` transformer on purpose: ingress.yaml sets its own
|
||||||
|
# namespace, and the transformer rewrites metadata.namespace on every resource
|
||||||
|
# it builds, which is a trap for anything cross-namespace added later.
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
# LLM API surface on the Kong gateway — DeepSeek/OpenAI-shaped.
|
||||||
|
#
|
||||||
|
# These live in namespace `llm-serving`, not `api`, because a Kubernetes Ingress
|
||||||
|
# can only reference a Service in its own namespace and the predictor Services
|
||||||
|
# are there. The Kong ingress controller watches all namespaces, so the routes
|
||||||
|
# still land on the gateway. They are synced by the `kong` Application (which
|
||||||
|
# has a `path: k8s/apps/api` source) so all gateway config stays in one place.
|
||||||
|
#
|
||||||
|
# ── Model -> upstream map (verified live) ───────────────────────────────────
|
||||||
|
# reasoning -> reasoning-predictor vLLM, DeepSeek-R1-Distill-32B, 2 replicas
|
||||||
|
# ornith:35b -> ornith-predictor Ollama, 2 replicas (retired verifier-
|
||||||
|
# qwen2.5:3b-instruct -> ornith-predictor Ollama predictor's vLLM PRM slot to get
|
||||||
|
# the 2nd GPU) -- k8s Service load-balances
|
||||||
|
# across both, each replica loads both
|
||||||
|
# models, so 2 concurrent implementer-style
|
||||||
|
# calls each land on an independent instance
|
||||||
|
# nomic-embed-text-v2 -> embeddings-predictor TEI
|
||||||
|
# bge-reranker-base -> reranker-predictor TEI
|
||||||
|
#
|
||||||
|
# ── Why path-per-model, and why the body is rewritten ───────────────────────
|
||||||
|
# Kong matches routes on host, path, method and headers — never on the request
|
||||||
|
# body. So a single /v1/chat/completions endpoint that dispatches on the body's
|
||||||
|
# `model` field is not expressible in Kong OSS (`ai-proxy-advanced`, which does
|
||||||
|
# multi-target model routing, is Enterprise-only).
|
||||||
|
#
|
||||||
|
# Hence the model is in the path, and each chat route force-overwrites `model`
|
||||||
|
# in the body regardless, so a client calling /v1/qwen/... with some other
|
||||||
|
# `model` value in the body can't silently get routed to the wrong weights.
|
||||||
|
# Callers may omit `model` entirely.
|
||||||
|
#
|
||||||
|
# ── Timeouts ───────────────────────────────────────────────────────────────
|
||||||
|
# Kong's upstream timeouts default to 60000ms. A 32B model generating a long
|
||||||
|
# answer on a Volta GPU routinely exceeds that, and the client would see a
|
||||||
|
# 504 mid-generation. Raised to 1h on every LLM route. Values are milliseconds.
|
||||||
|
|
||||||
|
# ── GET /v1/models ──────────────────────────────────────────────────────────
|
||||||
|
# Served entirely by Kong via request-termination: the plugin short-circuits in
|
||||||
|
# the access phase, so the backend below is never contacted. It only exists
|
||||||
|
# because an Ingress rule requires a backend.
|
||||||
|
#
|
||||||
|
# The list is static, which means it can drift from what the engines actually
|
||||||
|
# serve — notably if the Ollama pull list in the ornith InferenceService
|
||||||
|
# changes. Verify with:
|
||||||
|
# curl -s $SVC/v1/models (against each *-predictor)
|
||||||
|
apiVersion: configuration.konghq.com/v1
|
||||||
|
kind: KongPlugin
|
||||||
|
metadata:
|
||||||
|
name: llm-models-list
|
||||||
|
namespace: llm-serving
|
||||||
|
plugin: request-termination
|
||||||
|
config:
|
||||||
|
status_code: 200
|
||||||
|
content_type: application/json
|
||||||
|
body: |
|
||||||
|
{"object":"list","data":[
|
||||||
|
{"id":"reasoning","object":"model","owned_by":"homelab","created":0},
|
||||||
|
{"id":"ornith:35b","object":"model","owned_by":"homelab","created":0},
|
||||||
|
{"id":"qwen2.5:3b-instruct","object":"model","owned_by":"homelab","created":0},
|
||||||
|
{"id":"nomic-ai/nomic-embed-text-v2-moe","object":"model","owned_by":"homelab","created":0},
|
||||||
|
{"id":"BAAI/bge-reranker-base","object":"model","owned_by":"homelab","created":0}
|
||||||
|
]}
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: llm-models
|
||||||
|
namespace: llm-serving
|
||||||
|
annotations:
|
||||||
|
konghq.com/plugins: llm-models-list # model-key-auth stripped -- see model-auth.yaml
|
||||||
|
konghq.com/strip-path: "false"
|
||||||
|
konghq.com/methods: "GET"
|
||||||
|
spec:
|
||||||
|
ingressClassName: kong
|
||||||
|
rules:
|
||||||
|
- host: api.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /v1/models
|
||||||
|
pathType: Exact
|
||||||
|
backend:
|
||||||
|
# Never actually called — request-termination answers first.
|
||||||
|
service:
|
||||||
|
name: reasoning-predictor
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
---
|
||||||
|
# ── POST /v1/reasoning/chat/completions ─────────────────────────────────────
|
||||||
|
apiVersion: configuration.konghq.com/v1
|
||||||
|
kind: KongPlugin
|
||||||
|
metadata:
|
||||||
|
name: llm-rewrite-reasoning
|
||||||
|
namespace: llm-serving
|
||||||
|
plugin: request-transformer
|
||||||
|
config:
|
||||||
|
# `add` only applies when the field is absent, `replace` only when present.
|
||||||
|
# Both are needed to force the value in either case.
|
||||||
|
add:
|
||||||
|
body:
|
||||||
|
- "model:reasoning"
|
||||||
|
replace:
|
||||||
|
body:
|
||||||
|
- "model:reasoning"
|
||||||
|
# The model lives in the path for routing; the upstream still expects the
|
||||||
|
# canonical OpenAI path.
|
||||||
|
uri: /v1/chat/completions
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: llm-chat-reasoning
|
||||||
|
namespace: llm-serving
|
||||||
|
annotations:
|
||||||
|
konghq.com/plugins: llm-rewrite-reasoning # model-key-auth stripped -- see model-auth.yaml
|
||||||
|
konghq.com/strip-path: "false"
|
||||||
|
konghq.com/methods: "POST"
|
||||||
|
konghq.com/connect-timeout: "10000"
|
||||||
|
konghq.com/read-timeout: "3600000"
|
||||||
|
konghq.com/write-timeout: "3600000"
|
||||||
|
spec:
|
||||||
|
ingressClassName: kong
|
||||||
|
rules:
|
||||||
|
- host: api.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /v1/reasoning/chat/completions
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: reasoning-predictor
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
---
|
||||||
|
# ── POST /v1/ornith/chat/completions ────────────────────────────────────────
|
||||||
|
apiVersion: configuration.konghq.com/v1
|
||||||
|
kind: KongPlugin
|
||||||
|
metadata:
|
||||||
|
name: llm-rewrite-ornith
|
||||||
|
namespace: llm-serving
|
||||||
|
plugin: request-transformer
|
||||||
|
config:
|
||||||
|
add:
|
||||||
|
body:
|
||||||
|
- "model:ornith:35b"
|
||||||
|
replace:
|
||||||
|
body:
|
||||||
|
- "model:ornith:35b"
|
||||||
|
uri: /v1/chat/completions
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: llm-chat-ornith
|
||||||
|
namespace: llm-serving
|
||||||
|
annotations:
|
||||||
|
konghq.com/plugins: llm-rewrite-ornith # model-key-auth stripped -- see model-auth.yaml
|
||||||
|
konghq.com/strip-path: "false"
|
||||||
|
konghq.com/methods: "POST"
|
||||||
|
konghq.com/connect-timeout: "10000"
|
||||||
|
konghq.com/read-timeout: "3600000"
|
||||||
|
konghq.com/write-timeout: "3600000"
|
||||||
|
spec:
|
||||||
|
ingressClassName: kong
|
||||||
|
rules:
|
||||||
|
- host: api.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /v1/ornith/chat/completions
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: ornith-predictor
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
---
|
||||||
|
# ── POST /v1/qwen/chat/completions ──────────────────────────────────────────
|
||||||
|
# Same upstream pod as ornith — only the forced body `model` differs. Both stay
|
||||||
|
# resident because the engine runs with OLLAMA_MAX_LOADED_MODELS=2 and
|
||||||
|
# OLLAMA_KEEP_ALIVE=-1, so this does not trigger a model swap per request.
|
||||||
|
apiVersion: configuration.konghq.com/v1
|
||||||
|
kind: KongPlugin
|
||||||
|
metadata:
|
||||||
|
name: llm-rewrite-qwen
|
||||||
|
namespace: llm-serving
|
||||||
|
plugin: request-transformer
|
||||||
|
config:
|
||||||
|
add:
|
||||||
|
body:
|
||||||
|
- "model:qwen2.5:3b-instruct"
|
||||||
|
replace:
|
||||||
|
body:
|
||||||
|
- "model:qwen2.5:3b-instruct"
|
||||||
|
uri: /v1/chat/completions
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: llm-chat-qwen
|
||||||
|
namespace: llm-serving
|
||||||
|
annotations:
|
||||||
|
konghq.com/plugins: llm-rewrite-qwen # model-key-auth stripped -- see model-auth.yaml
|
||||||
|
konghq.com/strip-path: "false"
|
||||||
|
konghq.com/methods: "POST"
|
||||||
|
konghq.com/connect-timeout: "10000"
|
||||||
|
konghq.com/read-timeout: "3600000"
|
||||||
|
konghq.com/write-timeout: "3600000"
|
||||||
|
spec:
|
||||||
|
ingressClassName: kong
|
||||||
|
rules:
|
||||||
|
- host: api.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /v1/qwen/chat/completions
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: ornith-predictor
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
---
|
||||||
|
# ── POST /v1/embeddings ─────────────────────────────────────────────────────
|
||||||
|
# No path-per-model and no rewrite: there is exactly one embeddings backend, so
|
||||||
|
# there is nothing to disambiguate, and TEI already serves the canonical
|
||||||
|
# OpenAI path (verified: /v1/embeddings returns 405 to GET, i.e. it exists).
|
||||||
|
# That makes an OpenAI SDK a drop-in here.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: llm-embeddings
|
||||||
|
namespace: llm-serving
|
||||||
|
annotations:
|
||||||
|
konghq.com/strip-path: "false"
|
||||||
|
konghq.com/methods: "POST"
|
||||||
|
konghq.com/connect-timeout: "10000"
|
||||||
|
konghq.com/read-timeout: "600000"
|
||||||
|
konghq.com/write-timeout: "600000"
|
||||||
|
spec:
|
||||||
|
ingressClassName: kong
|
||||||
|
rules:
|
||||||
|
- host: api.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /v1/embeddings
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: embeddings-predictor
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
---
|
||||||
|
# ── POST /v1/rerank ─────────────────────────────────────────────────────────
|
||||||
|
# Rerank is not part of the OpenAI spec, and TEI serves it at /rerank — probing
|
||||||
|
# /v1/rerank returned 404 while /rerank returned 405, so this one genuinely
|
||||||
|
# needs the rewrite that embeddings does not.
|
||||||
|
apiVersion: configuration.konghq.com/v1
|
||||||
|
kind: KongPlugin
|
||||||
|
metadata:
|
||||||
|
name: llm-rewrite-rerank
|
||||||
|
namespace: llm-serving
|
||||||
|
plugin: request-transformer
|
||||||
|
config:
|
||||||
|
replace:
|
||||||
|
uri: /rerank
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: llm-rerank
|
||||||
|
namespace: llm-serving
|
||||||
|
annotations:
|
||||||
|
konghq.com/plugins: llm-rewrite-rerank # model-key-auth stripped -- see model-auth.yaml
|
||||||
|
konghq.com/strip-path: "false"
|
||||||
|
konghq.com/methods: "POST"
|
||||||
|
konghq.com/connect-timeout: "10000"
|
||||||
|
konghq.com/read-timeout: "600000"
|
||||||
|
konghq.com/write-timeout: "600000"
|
||||||
|
spec:
|
||||||
|
ingressClassName: kong
|
||||||
|
rules:
|
||||||
|
- host: api.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /v1/rerank
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: reranker-predictor
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# API auth layer — Kong key-auth on the model routes.
|
||||||
|
#
|
||||||
|
# TEMPORARILY RETIRED: verified live that Kong's key-auth here does not accept
|
||||||
|
# `Authorization: Bearer <key>` the way the comment below used to claim — a
|
||||||
|
# raw `apikey: <key>` header succeeds (200), the same request with only
|
||||||
|
# `Authorization: Bearer <key>` fails (401). No OpenAI-SDK-compatible client
|
||||||
|
# (pi included) sends a raw apikey header or lets you customize the header
|
||||||
|
# name, so every such client was hard-blocked. The KongPlugin below is
|
||||||
|
# commented out and every route's `konghq.com/plugins` annotation in
|
||||||
|
# llm-routes.yaml has `model-key-auth` stripped, so the model routes are
|
||||||
|
# unauthenticated for now. Re-enable once there's a Bearer-compatible fix
|
||||||
|
# (e.g. a request-transformer that copies the Bearer token into an `apikey`
|
||||||
|
# header before key-auth runs) — do not just uncomment this as-is, that
|
||||||
|
# reintroduces the exact block every real client hits.
|
||||||
|
#
|
||||||
|
# The key itself lives in the ksops-managed Secret model-invoke-apikey
|
||||||
|
# (labelled konghq.com/credential: key-auth) and is bound to the KongConsumer
|
||||||
|
# below, which stays defined (harmless without the plugin) so re-enabling
|
||||||
|
# later is a two-line uncomment instead of a rebuild.
|
||||||
|
---
|
||||||
|
apiVersion: configuration.konghq.com/v1
|
||||||
|
kind: KongConsumer
|
||||||
|
metadata:
|
||||||
|
name: model-invoker
|
||||||
|
namespace: api
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: kong
|
||||||
|
username: model-invoker
|
||||||
|
credentials:
|
||||||
|
- model-invoke-apikey
|
||||||
|
# ---
|
||||||
|
# apiVersion: configuration.konghq.com/v1
|
||||||
|
# kind: KongPlugin
|
||||||
|
# metadata:
|
||||||
|
# name: model-key-auth
|
||||||
|
# namespace: llm-serving
|
||||||
|
# plugin: key-auth
|
||||||
|
# config:
|
||||||
|
# key_names:
|
||||||
|
# - apikey
|
||||||
|
# - authorization
|
||||||
|
# key_in_header: true
|
||||||
|
# key_in_query: false
|
||||||
|
# key_in_body: false
|
||||||
|
# hide_credentials: true
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
cloudflared:
|
|
||||||
tunnelToken: ENC[AES256_GCM,data:GrZNC75M1T304m+MBbgcL9Wa6VD3Cc+joERjUWD7PsI5NlqFpVuorzub4OTaiE/F0sqDXtXdSwQP9jGTkgUnNbviUVEIBDb+zuiyzCfzuhc53oMvoUYRkRNeH5DR9TBEszspj8+mjQMDAKQFKtTmyDodgh8DdPg8LE8YTCqyx6CcpkGZ8yWY06VoKpZfFSOan/gHwRwG2500P8U+rzI676EKVUhWjBFP0iTbwDQR7spI7oz8Gon/0Q==,iv:O3i0v+M5L3i9O7SbBtDAJe5IsQDgw+alI0Y9arZNojs=,tag:/T63FivzvbLFaXEaBsn5FA==,type:str]
|
|
||||||
sops:
|
|
||||||
age:
|
|
||||||
- enc: |
|
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpU2tjOGpJNk9leE9LSnlx
|
|
||||||
b2tVbFFMMkZ3OHZuU0VhcUp5Tjl4ZDh2R1U4CjFFMURDWFJtN1lETUNpSmEwbU9u
|
|
||||||
aEIxUW5qWTJGTnlobjV0emlpY00rM28KLS0tIFd0cllZeXFodFhJNTJMNkRLNyt1
|
|
||||||
cXVyNnM4Y2pXeUFTYzU1OXlaOXR2RWcKp7/M/YFfpJg4L6a0AcYTV3Rza+bzaOeD
|
|
||||||
OUIwyns8ZsPtU8ILbRYUUdD2EJFiOPnWP4yX70Ak10v12gfB7vRJ6A==
|
|
||||||
-----END AGE ENCRYPTED FILE-----
|
|
||||||
recipient: age1smu533f803gmd0jq60s2zaj9zlznajy0ca6rtewd4r37mr2hs3uqsrldfh
|
|
||||||
lastmodified: "2026-07-20T17:49:35Z"
|
|
||||||
mac: ENC[AES256_GCM,data:MhuuuInJjGbzoftxVqMZUov1tJpOt5Vb8GwTr0R12hRCVHwdV+cuC1YeXTQidYaduO7neGnYe0p8ESLEpyY06j4nSpCvGRPPaxTeSeA8IcIk71xNtA1X0FVPv51s59rpvVSjiMDCqrcOv1aJybfQnBZoc/9bfmCYGqhDkD6sg1U=,iv:ThTjUGYE9GiyIgXS+0KDLYS20RPJjMG9DKyMSoKGk/4=,tag:KqZYps5qSuKPUI4U3Iabmw==,type:str]
|
|
||||||
unencrypted_suffix: _unencrypted
|
|
||||||
version: 3.13.2
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: duckdns-updater
|
|
||||||
namespace: kube-system
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: duckdns-updater
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: duckdns-updater
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: updater
|
|
||||||
image: curlimages/curl:latest
|
|
||||||
command:
|
|
||||||
- sh
|
|
||||||
- -c
|
|
||||||
- |
|
|
||||||
while true; do
|
|
||||||
curl -fsS "https://www.duckdns.org/update?domains=riotpiao&token=${DUCKDNS_TOKEN}&ip="
|
|
||||||
sleep 300
|
|
||||||
done
|
|
||||||
env:
|
|
||||||
- name: DUCKDNS_TOKEN
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: duckdns-token
|
|
||||||
key: token
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: 5m
|
|
||||||
memory: 16Mi
|
|
||||||
limits:
|
|
||||||
cpu: 50m
|
|
||||||
memory: 32Mi
|
|
||||||
restartPolicy: Always
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
resources:
|
|
||||||
- duckdns-corn.yaml
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
# PostSync hook to patch Homarr deployment probes
|
|
||||||
# Chart v8.23.0 doesn't support probe customization via values
|
|
||||||
apiVersion: batch/v1
|
|
||||||
kind: Job
|
|
||||||
metadata:
|
|
||||||
name: homarr-fix-probes
|
|
||||||
namespace: dashboard
|
|
||||||
annotations:
|
|
||||||
argocd.argoproj.io/hook: PostSync
|
|
||||||
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
|
|
||||||
spec:
|
|
||||||
backoffLimit: 3
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
restartPolicy: Never
|
|
||||||
serviceAccountName: homarr-probe-patcher
|
|
||||||
containers:
|
|
||||||
- name: patch
|
|
||||||
image: python:3.12-alpine
|
|
||||||
command:
|
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- |
|
|
||||||
set -e
|
|
||||||
echo "Installing kubectl..."
|
|
||||||
wget -q -O /tmp/kubectl https://dl.k8s.io/release/v1.28.0/bin/linux/amd64/kubectl
|
|
||||||
chmod +x /tmp/kubectl
|
|
||||||
|
|
||||||
echo "Patching Homarr deployment probes..."
|
|
||||||
/tmp/kubectl -n dashboard patch deployment homarr --type=json -p='[
|
|
||||||
{
|
|
||||||
"op": "replace",
|
|
||||||
"path": "/spec/template/spec/containers/0/livenessProbe/initialDelaySeconds",
|
|
||||||
"value": 60
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"op": "replace",
|
|
||||||
"path": "/spec/template/spec/containers/0/livenessProbe/periodSeconds",
|
|
||||||
"value": 30
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"op": "replace",
|
|
||||||
"path": "/spec/template/spec/containers/0/livenessProbe/timeoutSeconds",
|
|
||||||
"value": 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"op": "replace",
|
|
||||||
"path": "/spec/template/spec/containers/0/readinessProbe/initialDelaySeconds",
|
|
||||||
"value": 45
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"op": "replace",
|
|
||||||
"path": "/spec/template/spec/containers/0/readinessProbe/periodSeconds",
|
|
||||||
"value": 15
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"op": "replace",
|
|
||||||
"path": "/spec/template/spec/containers/0/readinessProbe/timeoutSeconds",
|
|
||||||
"value": 5
|
|
||||||
}
|
|
||||||
]'
|
|
||||||
|
|
||||||
echo "✅ Probes patched successfully"
|
|
||||||
echo " Liveness: 60s initial, 30s period, 5s timeout"
|
|
||||||
echo " Readiness: 45s initial, 15s period, 5s timeout"
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: homarr-probe-patcher
|
|
||||||
namespace: dashboard
|
|
||||||
---
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: Role
|
|
||||||
metadata:
|
|
||||||
name: homarr-probe-patcher
|
|
||||||
namespace: dashboard
|
|
||||||
rules:
|
|
||||||
- apiGroups: ["apps"]
|
|
||||||
resources: ["deployments"]
|
|
||||||
verbs: ["get", "patch"]
|
|
||||||
---
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: RoleBinding
|
|
||||||
metadata:
|
|
||||||
name: homarr-probe-patcher
|
|
||||||
namespace: dashboard
|
|
||||||
roleRef:
|
|
||||||
apiGroup: rbac.authorization.k8s.io
|
|
||||||
kind: Role
|
|
||||||
name: homarr-probe-patcher
|
|
||||||
subjects:
|
|
||||||
- kind: ServiceAccount
|
|
||||||
name: homarr-probe-patcher
|
|
||||||
namespace: dashboard
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
apiVersion: ENC[AES256_GCM,data:XZc=,iv:0Lgui0+X2oTlgCX5HEIIihEsGJD0C2+pDIOTCNi4r+g=,tag:U2DPsVJ1gCoZw1YcgmT7UA==,type:str]
|
|
||||||
kind: ENC[AES256_GCM,data:SRkjGwsv,iv:KRBfS7COsTs/l3Fhyk5FvH7IVc+r/6M6FVOo+kPDe1Q=,tag:113xdFELRoJiU4Kn8BmvPA==,type:str]
|
|
||||||
metadata:
|
|
||||||
name: ENC[AES256_GCM,data:stIxzQqOUvpbEANL4aQ=,iv:Q+DCLGtw3wihhdAgEbzYHyQhFTPWFyE2iJKl9mYaA2E=,tag:2XRGRbPQLNUGbA1dZI9xEQ==,type:str]
|
|
||||||
namespace: ENC[AES256_GCM,data:wIhhcrQi3cDF,iv:gnCCkV7GKXA2HWOOeNUGnpAycf8U7pl6F8Vfo6DhLR4=,tag:xnqvCtUdFPB5D9meJb36GA==,type:str]
|
|
||||||
type: ENC[AES256_GCM,data:1qUDnlHH,iv:gBaC+6MXvZY/bIpy/cEG/zWwLBaUFMVfxAHa1OnwfnQ=,tag:u9PcCfvA2BydFNQQKws1Og==,type:str]
|
|
||||||
stringData:
|
|
||||||
SECRET_ENCRYPTION_KEY: ENC[AES256_GCM,data:b0h8ekf7RIbQ9YTpaXPmR3aeNonk60lgltPmnRe7lVk9wlAdaXkNChINcAsarw9YhfYvfCZfS8U33/QxLGjaYA==,iv:uOmx8w3XPKnoxfasf7gqi/bljxigpLwaAC30c2TroKg=,tag:JphkeY49bQLm6cPjT6g8LA==,type:str]
|
|
||||||
sops:
|
|
||||||
age:
|
|
||||||
- enc: |
|
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiUW1BVmEvTnJqa3RuQ3NG
|
|
||||||
UFg3SE1SclpWd01TZnFOK0trQ1c2YThUQmpRCkdGc3FJSmRCcFIycWcvZXJCbDdj
|
|
||||||
T1M2cFFUakpOa2FST1k2UHc0a1hCRWMKLS0tIDk0UVdYRUkvTEM4TEVWRElrY3dN
|
|
||||||
SFU2bkZxUGlDL0JNSnY3UU54Z3BhbFUKnJYwwQkUveNjTF9CveVUBF8RO7QjDnYA
|
|
||||||
1+COKzvXuCIupvPcS87AtkrOfbfTu3IK78pfxZn04FmMOuAyj0ynzA==
|
|
||||||
-----END AGE ENCRYPTED FILE-----
|
|
||||||
recipient: age1smu533f803gmd0jq60s2zaj9zlznajy0ca6rtewd4r37mr2hs3uqsrldfh
|
|
||||||
lastmodified: "2026-07-22T15:59:29Z"
|
|
||||||
mac: ENC[AES256_GCM,data:uMmKD4fvu39KZc2lDoDD25g/p/eIGW/CYFIwYXcRFPdbLLLi+iW9ks/2l5tHqzlwHEtlL1RH4HLOQr25k3GAJA2FhVnyVmyZ4N2EnJfic/9XTfef9E8tKVo2cXEgYMCPbpH8sVu2OoaQ9BFw8dTM9ZxuTdGKksGOtGp56I4R8hA=,iv:QFhVAZ1jykTNLPDzqjYf9jkg5PuqYA8+s0Hn57ND3KA=,tag:Zz2qYANQP33izwg+3Ap16A==,type:str]
|
|
||||||
unencrypted_suffix: _unencrypted
|
|
||||||
version: 3.13.2
|
|
||||||
@@ -1,5 +1,22 @@
|
|||||||
# Homarr landing page with Authentik SSO
|
# Homarr landing page with Authentik SSO
|
||||||
# Probes patched via PostSync hook (chart doesn't support customization)
|
|
||||||
|
# Probe tuning (chart DOES expose these — the old PostSync patch-job was
|
||||||
|
# unnecessary and fragile: it only patched one Deployment revision, so any later
|
||||||
|
# rollout reverted to the chart's aggressive defaults). Homarr's first-boot icon
|
||||||
|
# updater blocks the event loop for ~50s ("icons updater took 49553ms"), during
|
||||||
|
# which /api/health/live can't answer within the default 10s×3 window → kubelet
|
||||||
|
# SIGTERMs the pod → CrashLoopBackOff (247 restarts, 503 at the ingress). Give
|
||||||
|
# liveness a wide window so the icon import can finish without a kill.
|
||||||
|
livenessProbe:
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
periodSeconds: 30
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 10
|
||||||
|
readinessProbe:
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 15
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 6
|
||||||
|
|
||||||
image:
|
image:
|
||||||
repository: ghcr.io/homarr-labs/homarr
|
repository: ghcr.io/homarr-labs/homarr
|
||||||
@@ -13,10 +30,19 @@ replicaCount: 1
|
|||||||
env:
|
env:
|
||||||
AUTH_PROVIDERS: "oidc,credentials"
|
AUTH_PROVIDERS: "oidc,credentials"
|
||||||
AUTH_OIDC_ISSUER: "https://authentik.riotpiao.com/application/o/homarr/"
|
AUTH_OIDC_ISSUER: "https://authentik.riotpiao.com/application/o/homarr/"
|
||||||
|
# AUTH_OIDC_URI (authorize endpoint) is REQUIRED in addition to ISSUER — homarr
|
||||||
|
# hides the "Sign in with Authentik" button entirely when it's absent (per the
|
||||||
|
# authentik Homarr integration + homarr SSO docs). This was the missing var.
|
||||||
|
AUTH_OIDC_URI: "https://authentik.riotpiao.com/application/o/authorize/"
|
||||||
AUTH_OIDC_CLIENT_NAME: "Authentik"
|
AUTH_OIDC_CLIENT_NAME: "Authentik"
|
||||||
AUTH_OIDC_GROUPS_ATTRIBUTE: "groups"
|
AUTH_OIDC_GROUPS_ATTRIBUTE: "groups"
|
||||||
AUTH_OIDC_SCOPE_OVERWRITE: "openid email profile groups"
|
AUTH_OIDC_SCOPE_OVERWRITE: "openid email profile groups"
|
||||||
AUTH_OIDC_AUTO_LOGIN: "false"
|
AUTH_OIDC_AUTO_LOGIN: "false"
|
||||||
|
# Link the OIDC identity to an existing homarr account with the same email.
|
||||||
|
OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING: "true"
|
||||||
|
# The analytics cron blocked the (single-threaded) Next.js event loop for ~16s
|
||||||
|
# per run ("callback took longer than expected"), compounding CPU pressure.
|
||||||
|
DISABLE_ANALYTICS: "true"
|
||||||
BASE_URL: "https://homarr.riotpiao.com"
|
BASE_URL: "https://homarr.riotpiao.com"
|
||||||
NEXTAUTH_URL: "https://homarr.riotpiao.com"
|
NEXTAUTH_URL: "https://homarr.riotpiao.com"
|
||||||
|
|
||||||
@@ -41,8 +67,12 @@ tolerations:
|
|||||||
|
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: 100m
|
cpu: 250m
|
||||||
memory: 256Mi
|
memory: 384Mi
|
||||||
limits:
|
limits:
|
||||||
cpu: 500m
|
# Next.js 16 + bundled redis + the icon-updater (28k icons) saturated the old
|
||||||
memory: 512Mi
|
# 500m limit; CPU throttling made Next.js abort with exit 134 (SIGABRT) and
|
||||||
|
# self-restart in a loop, so nginx saw no upstream and returned 502. Give it
|
||||||
|
# real CPU headroom.
|
||||||
|
cpu: "2"
|
||||||
|
memory: 1Gi
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
namespace: dashboard
|
namespace: dashboard
|
||||||
resources:
|
# Probes are now tuned via homarr-values.yaml (chart-native); the old
|
||||||
- fix-probes-job.yaml
|
# fix-probes-job PostSync hook is removed. homarr-secrets/auth-oidc/db-encryption
|
||||||
# PostSync hook to patch Homarr deployment probes
|
# Secrets are delivered by the sops-secrets (ksops) Application.
|
||||||
# (homarr-secrets.enc.yaml managed by sops-secrets Application)
|
resources: []
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
apiVersion: serving.kserve.io/v1beta1
|
||||||
|
kind: InferenceService
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
serving.kserve.io/deploymentMode: RawDeployment
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: llm-embeddings
|
||||||
|
app.kubernetes.io/part-of: llm-serving
|
||||||
|
name: embeddings
|
||||||
|
namespace: llm-serving
|
||||||
|
spec:
|
||||||
|
predictor:
|
||||||
|
containers:
|
||||||
|
- args:
|
||||||
|
- --model-id=nomic-ai/nomic-embed-text-v2-moe
|
||||||
|
- --port=8080
|
||||||
|
- --hostname=0.0.0.0
|
||||||
|
- --auto-truncate
|
||||||
|
env:
|
||||||
|
- name: HUGGINGFACE_HUB_CACHE
|
||||||
|
value: /mnt/models
|
||||||
|
image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.8.2@sha256:4d632b76bd14cb57044a1ffb0ad48ab0ba4939e705a9a615ccc740658575c26e
|
||||||
|
name: kserve-container
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: '16'
|
||||||
|
memory: 8Gi
|
||||||
|
requests:
|
||||||
|
cpu: '8'
|
||||||
|
memory: 4Gi
|
||||||
|
startupProbe:
|
||||||
|
failureThreshold: 60
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
periodSeconds: 10
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /mnt/models
|
||||||
|
name: models
|
||||||
|
maxReplicas: 1
|
||||||
|
minReplicas: 1
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: worker-1
|
||||||
|
volumes:
|
||||||
|
- name: models
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: llm-models
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
# Explicit allowlist, matching k8s/apps/api. Anything added to this directory
|
||||||
|
# and not listed here is silently dropped — no error, no drift shown.
|
||||||
|
#
|
||||||
|
# These five were adopted from live state on 2026-08-15; they had been applied
|
||||||
|
# by hand and carried no ArgoCD ownership. Each was exported and verified with
|
||||||
|
# `kubectl diff -f <file>` returning empty before the Application below was
|
||||||
|
# created, so the first sync was a no-op rather than a redeploy. Re-verify that
|
||||||
|
# way after any edit here: a GPU predictor restart is a weights reload measured
|
||||||
|
# in tens of seconds, not a rolling update.
|
||||||
|
resources:
|
||||||
|
- embeddings.yaml
|
||||||
|
- ornith.yaml
|
||||||
|
- reasoning.yaml
|
||||||
|
- reranker.yaml
|
||||||
|
# No namespace transformer: every file sets its own, and the transformer would
|
||||||
|
# rewrite metadata.namespace on anything cross-namespace added later.
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
apiVersion: serving.kserve.io/v1beta1
|
||||||
|
kind: InferenceService
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
serving.kserve.io/deploymentMode: RawDeployment
|
||||||
|
# Kong reads its timeouts from the Kubernetes Service, not the Ingress —
|
||||||
|
# Ingress annotations configure Route entities (strip-path, methods,
|
||||||
|
# plugins), these configure the Service entity. They were on
|
||||||
|
# llm-chat-ornith's Ingress and therefore ignored, leaving Kong's 60s
|
||||||
|
# default in force. KServe propagates InferenceService annotations to the
|
||||||
|
# Service it generates, which is how they reach Kong from here.
|
||||||
|
#
|
||||||
|
# This was invisible while OLLAMA_KEEP_ALIVE=-1 kept the model resident: no
|
||||||
|
# request ever waited on a cold load. A pod restart flushes VRAM, and
|
||||||
|
# loading ornith:35b takes longer than 60s, so the first request after any
|
||||||
|
# restart returned 504.
|
||||||
|
konghq.com/connect-timeout: "10000"
|
||||||
|
konghq.com/read-timeout: "3600000"
|
||||||
|
konghq.com/write-timeout: "3600000"
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: llm-ornith
|
||||||
|
app.kubernetes.io/part-of: llm-serving
|
||||||
|
name: ornith
|
||||||
|
namespace: llm-serving
|
||||||
|
spec:
|
||||||
|
predictor:
|
||||||
|
containers:
|
||||||
|
- command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- 'set -e
|
||||||
|
|
||||||
|
ollama serve &
|
||||||
|
|
||||||
|
SERVE_PID=$!
|
||||||
|
|
||||||
|
until ollama list >/dev/null 2>&1; do sleep 2; done
|
||||||
|
|
||||||
|
ollama pull ornith:35b
|
||||||
|
|
||||||
|
ollama pull qwen2.5:3b-instruct
|
||||||
|
|
||||||
|
ollama run ornith:35b "ok" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
ollama run qwen2.5:3b-instruct "ok" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
wait $SERVE_PID
|
||||||
|
|
||||||
|
'
|
||||||
|
env:
|
||||||
|
- name: OLLAMA_HOST
|
||||||
|
value: 0.0.0.0:8080
|
||||||
|
- name: OLLAMA_MODELS
|
||||||
|
value: /mnt/models/ollama
|
||||||
|
- name: OLLAMA_CONTEXT_LENGTH
|
||||||
|
value: '32768'
|
||||||
|
- name: OLLAMA_KEEP_ALIVE
|
||||||
|
value: '-1'
|
||||||
|
- name: OLLAMA_NUM_PARALLEL
|
||||||
|
value: '1'
|
||||||
|
- name: OLLAMA_MAX_LOADED_MODELS
|
||||||
|
value: '2'
|
||||||
|
image: ollama/ollama:0.32.9@sha256:1685741456770df6e3cceb2a945a5f75e020f658d1701509668d6f4688f1dd3f
|
||||||
|
name: kserve-container
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- ollama ps 2>/dev/null | grep -q ornith && ollama ps 2>/dev/null |
|
||||||
|
grep -q qwen2.5
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: '16'
|
||||||
|
memory: 16Gi
|
||||||
|
nvidia.com/gpu: '1'
|
||||||
|
requests:
|
||||||
|
cpu: '8'
|
||||||
|
memory: 8Gi
|
||||||
|
nvidia.com/gpu: '1'
|
||||||
|
startupProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- ollama ps 2>/dev/null | grep -q ornith && ollama ps 2>/dev/null |
|
||||||
|
grep -q qwen2.5
|
||||||
|
failureThreshold: 120
|
||||||
|
periodSeconds: 15
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /mnt/models
|
||||||
|
name: models
|
||||||
|
deploymentStrategy:
|
||||||
|
type: Recreate
|
||||||
|
# 2 replicas -- each its own GPU, each loading both ornith:35b and
|
||||||
|
# qwen2.5:3b-instruct -- so 2 concurrent implementer-style calls each
|
||||||
|
# get an independent instance instead of contending on one, at the
|
||||||
|
# cost of judge/qwen traffic still sharing whichever replica an
|
||||||
|
# implementer call also lands on.
|
||||||
|
maxReplicas: 2
|
||||||
|
minReplicas: 2
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: worker-1
|
||||||
|
runtimeClassName: nvidia
|
||||||
|
volumes:
|
||||||
|
- name: models
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: llm-models
|
||||||
|
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
apiVersion: serving.kserve.io/v1beta1
|
||||||
|
kind: InferenceService
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
serving.kserve.io/deploymentMode: RawDeployment
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: llm-reasoning
|
||||||
|
app.kubernetes.io/part-of: llm-serving
|
||||||
|
name: reasoning
|
||||||
|
namespace: llm-serving
|
||||||
|
spec:
|
||||||
|
predictor:
|
||||||
|
containers:
|
||||||
|
- args:
|
||||||
|
# DeepSeek-R1-Distill-32B retired: tool_choice="auto" (what pi sends)
|
||||||
|
# hit a documented vLLM/R1-family conflict -- the model narrated fake
|
||||||
|
# tool_calls in its <think> block instead of emitting real ones,
|
||||||
|
# regardless of parser. Tried swapping to a Kimi-distilled Qwen3.6
|
||||||
|
# MoE checkpoint and an AWQ-quantized Qwen3-30B-A3B first -- both
|
||||||
|
# failed on real, separate blockers (unrecognized model_type; then
|
||||||
|
# marlin INT4 kernels needing compute capability 80+, but worker-1's
|
||||||
|
# GPU is sm70/V100). Landed on dense Qwen3-32B instead: native Qwen3
|
||||||
|
# tool-call format (no narration bug), bnb-4bit works fine on sm70
|
||||||
|
# (proven by the old DeepSeek config already), and no MoE
|
||||||
|
# arch/quantization risk this time.
|
||||||
|
- --model=unsloth/Qwen3-32B-bnb-4bit
|
||||||
|
- --served-model-name=reasoning
|
||||||
|
- --quantization=bitsandbytes
|
||||||
|
- --dtype=float16
|
||||||
|
- --kv-cache-dtype=auto
|
||||||
|
- --tensor-parallel-size=1
|
||||||
|
- --max-model-len=16384
|
||||||
|
- --gpu-memory-utilization=0.90
|
||||||
|
- --max-num-seqs=4
|
||||||
|
- --enable-chunked-prefill
|
||||||
|
- --enable-prefix-caching
|
||||||
|
# qwen3 is vLLM's dedicated reasoning parser for this family's <think>
|
||||||
|
# blocks.
|
||||||
|
- --reasoning-parser=qwen3
|
||||||
|
# hermes is the documented tool-call parser for general (non-Coder)
|
||||||
|
# Qwen3 models -- native chat template support, not narrated text.
|
||||||
|
- --enable-auto-tool-choice
|
||||||
|
- --tool-call-parser=hermes
|
||||||
|
# vLLM 0.11.0's native OffloadingConnector -- spills KV cache blocks
|
||||||
|
# to CPU DRAM instead of discarding them on preemption (max-num-seqs=4
|
||||||
|
# + max-model-len=16384 means concurrent long sequences compete for
|
||||||
|
# the same GPU KV space). No extra dependency, built into vLLM core.
|
||||||
|
# num_cpu_blocks=2000 hung the pod at startup on the old model (2000 x
|
||||||
|
# ~32MB/block blew well past the pod's memory limit). num_cpu_blocks=32
|
||||||
|
# was the safe-recovery value after that -- only ~1GB of real DRAM
|
||||||
|
# (32 blocks x 128 tokens x 256KB/token-across-all-64-layers, fp16),
|
||||||
|
# basically a token-count safety valve, not real offload capacity.
|
||||||
|
# This model: 64 layers, 8 KV heads x 128 head_dim, fp16 -> ~256KB of
|
||||||
|
# KV per token across all layers -> ~32MB per 128-token block.
|
||||||
|
# num_cpu_blocks=256 -> ~8GB of actual DRAM offload (32,768 tokens),
|
||||||
|
# comfortably under the pod's 36Gi limit alongside the ~20GB bnb-4bit
|
||||||
|
# weights. Watch real host memory on boot before raising further --
|
||||||
|
# block_size=128 tokens matches vLLM's own example.
|
||||||
|
# Note: 0.11.0 ships the original (fragmented, small-transfer-block)
|
||||||
|
# version of this connector -- 0.12.0 consolidates KV data into one
|
||||||
|
# contiguous block per request and is reported an order of magnitude
|
||||||
|
# faster for this specific feature, so this is a real but not yet
|
||||||
|
# optimal implementation until the image gets bumped.
|
||||||
|
- --kv-transfer-config={"kv_connector":"OffloadingConnector","kv_role":"kv_both","kv_connector_extra_config":{"num_cpu_blocks":256,"block_size":128}}
|
||||||
|
- --host=0.0.0.0
|
||||||
|
- --port=8080
|
||||||
|
env:
|
||||||
|
- name: VLLM_USE_FLASHINFER_SAMPLER
|
||||||
|
value: '0'
|
||||||
|
- name: VLLM_ATTENTION_BACKEND
|
||||||
|
value: TRITON_ATTN
|
||||||
|
- name: HF_HOME
|
||||||
|
value: /mnt/models
|
||||||
|
image: vllm/vllm-openai:v0.11.0@sha256:014a95f21c9edf6abe0aea6b07353f96baa4ec291c427bb1176dc7c93a85845c
|
||||||
|
name: kserve-container
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: '16'
|
||||||
|
memory: 36Gi
|
||||||
|
nvidia.com/gpu: '1'
|
||||||
|
requests:
|
||||||
|
cpu: '8'
|
||||||
|
memory: 12Gi
|
||||||
|
nvidia.com/gpu: '1'
|
||||||
|
startupProbe:
|
||||||
|
failureThreshold: 80
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
periodSeconds: 15
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /mnt/models
|
||||||
|
name: models
|
||||||
|
- mountPath: /dev/shm
|
||||||
|
name: shm
|
||||||
|
deploymentStrategy:
|
||||||
|
type: Recreate
|
||||||
|
maxReplicas: 2
|
||||||
|
minReplicas: 2
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: worker-1
|
||||||
|
runtimeClassName: nvidia
|
||||||
|
volumes:
|
||||||
|
- name: models
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: llm-models
|
||||||
|
- emptyDir:
|
||||||
|
medium: Memory
|
||||||
|
sizeLimit: 2Gi
|
||||||
|
name: shm
|
||||||
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
apiVersion: serving.kserve.io/v1beta1
|
||||||
|
kind: InferenceService
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
serving.kserve.io/deploymentMode: RawDeployment
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: llm-reranker
|
||||||
|
app.kubernetes.io/part-of: llm-serving
|
||||||
|
name: reranker
|
||||||
|
namespace: llm-serving
|
||||||
|
spec:
|
||||||
|
predictor:
|
||||||
|
containers:
|
||||||
|
- args:
|
||||||
|
- --model-id=BAAI/bge-reranker-base
|
||||||
|
- --port=8080
|
||||||
|
- --hostname=0.0.0.0
|
||||||
|
- --auto-truncate
|
||||||
|
env:
|
||||||
|
- name: HUGGINGFACE_HUB_CACHE
|
||||||
|
value: /mnt/models
|
||||||
|
image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.8.2@sha256:4d632b76bd14cb57044a1ffb0ad48ab0ba4939e705a9a615ccc740658575c26e
|
||||||
|
name: kserve-container
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: '16'
|
||||||
|
memory: 8Gi
|
||||||
|
requests:
|
||||||
|
cpu: '8'
|
||||||
|
memory: 4Gi
|
||||||
|
startupProbe:
|
||||||
|
failureThreshold: 60
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
periodSeconds: 10
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /mnt/models
|
||||||
|
name: models
|
||||||
|
maxReplicas: 1
|
||||||
|
minReplicas: 1
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: worker-1
|
||||||
|
volumes:
|
||||||
|
- name: models
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: llm-models
|
||||||
|
|
||||||
@@ -10,13 +10,22 @@ metadata:
|
|||||||
name: queue-operator
|
name: queue-operator
|
||||||
rules:
|
rules:
|
||||||
- apiGroups: ["kmsvc.io"]
|
- apiGroups: ["kmsvc.io"]
|
||||||
resources: ["queues", "temporalworkers"]
|
resources: ["queues"]
|
||||||
verbs: ["get", "list", "watch", "update", "patch"]
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
- apiGroups: ["kmsvc.io"]
|
- apiGroups: ["kmsvc.io"]
|
||||||
resources: ["queues/status", "temporalworkers/status"]
|
resources: ["queues/status"]
|
||||||
verbs: ["get", "update", "patch"]
|
verbs: ["get", "update", "patch"]
|
||||||
- apiGroups: ["kmsvc.io"]
|
- apiGroups: ["kmsvc.io"]
|
||||||
resources: ["queues/finalizers", "temporalworkers/finalizers"]
|
resources: ["queues/finalizers"]
|
||||||
|
verbs: ["update"]
|
||||||
|
- apiGroups: ["kmsvc.io"]
|
||||||
|
resources: ["temporalworkers"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
- apiGroups: ["kmsvc.io"]
|
||||||
|
resources: ["temporalworkers/status"]
|
||||||
|
verbs: ["get", "update", "patch"]
|
||||||
|
- apiGroups: ["kmsvc.io"]
|
||||||
|
resources: ["temporalworkers/finalizers"]
|
||||||
verbs: ["update"]
|
verbs: ["update"]
|
||||||
- apiGroups: ["coordination.k8s.io"]
|
- apiGroups: ["coordination.k8s.io"]
|
||||||
resources: ["leases"]
|
resources: ["leases"]
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ tls:
|
|||||||
# RWO block volume. 10Gi is generous for config data but cheap on Longhorn.
|
# RWO block volume. 10Gi is generous for config data but cheap on Longhorn.
|
||||||
persistence:
|
persistence:
|
||||||
enabled: true
|
enabled: true
|
||||||
storageClass: "longhorn-wffc"
|
storageClass: "longhorn"
|
||||||
size: 10Gi
|
size: 10Gi
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
@@ -52,7 +52,8 @@ tolerations:
|
|||||||
operator: Exists
|
operator: Exists
|
||||||
effect: NoSchedule
|
effect: NoSchedule
|
||||||
|
|
||||||
# Pin to az-a (talos-cp-1) — sole Longhorn storage node. Its RWO PVC can only
|
# Pin to az-b (talos-cp-2) — sole Longhorn storage node (dedicated disks).
|
||||||
# attach there; without this the pod may land on cp-2/cp-3 and fail to mount.
|
# Its RWO PVC can only attach there; without this the pod may land on
|
||||||
|
# cp-1/cp-3 and fail to mount.
|
||||||
nodeSelector:
|
nodeSelector:
|
||||||
topology.kubernetes.io/zone: az-a
|
topology.kubernetes.io/zone: az-b
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# macOS VM (Docker-OSX) hosting the BlueBubbles server.
|
||||||
|
#
|
||||||
|
# ── Why a VM and not a container ────────────────────────────────────────────
|
||||||
|
# Containers share the host kernel. macOS binaries are Mach-O and need XNU plus
|
||||||
|
# Cocoa/IOKit, which a Linux kernel cannot provide, so no macOS container exists
|
||||||
|
# or can exist. Docker-OSX is QEMU running a macOS guest, packaged in a
|
||||||
|
# container — a VM in a box, not a macOS container.
|
||||||
|
#
|
||||||
|
# ── Why this works on worker-2 ──────────────────────────────────────────────
|
||||||
|
# Verified on the existing hardware: amd64, `vmx` (Intel VT-x) present, and
|
||||||
|
# /dev/kvm exists on Talos nodes (KVM is compiled into Talos' kernel, not a
|
||||||
|
# module). Bare metal, so no nested virtualisation needed.
|
||||||
|
#
|
||||||
|
# ── Read this before relying on it ──────────────────────────────────────────
|
||||||
|
# 1. Setup is INTERACTIVE. First boot runs the macOS installer: connect over
|
||||||
|
# VNC (:5999), erase the disk in Disk Utility, install, create a user, sign
|
||||||
|
# into iMessage, THEN install BlueBubbles inside the guest. This manifest
|
||||||
|
# only provides the machine; it does not provision macOS.
|
||||||
|
# 2. iMessage activation on non-Apple hardware is a coin flip. BlueBubbles'
|
||||||
|
# own guidance: "test sending an iMessage to yourself. If it does not
|
||||||
|
# succeed, it's likely best to restart from the beginning."
|
||||||
|
# 3. Apple's macOS licence permits virtualisation only on Apple hardware. This
|
||||||
|
# is a Hackintosh. Use a throwaway Apple ID, not a primary one.
|
||||||
|
# 4. BlueBubbles labels this path "not for beginners", "no guarantees or
|
||||||
|
# warranty".
|
||||||
|
#
|
||||||
|
# Private API (reactions, typing indicators, edit/unsend) needs SIP disabled
|
||||||
|
# inside the guest and is NOT required for plain send/receive. Skip it.
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: macos-bluebubbles
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: macos-bluebubbles
|
||||||
|
app.kubernetes.io/part-of: sms
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
# Recreate: the qcow2 disk is RWO and a second pod must never attach it
|
||||||
|
# concurrently — two QEMU processes on one image corrupts it.
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: macos-bluebubbles
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: macos-bluebubbles
|
||||||
|
app.kubernetes.io/part-of: sms
|
||||||
|
spec:
|
||||||
|
# Dedicated node. The taint keeps everything else off worker-2; this
|
||||||
|
# toleration is what lets the VM on. Both halves are required.
|
||||||
|
nodeSelector:
|
||||||
|
workload: imessage
|
||||||
|
tolerations:
|
||||||
|
- key: workload
|
||||||
|
operator: Equal
|
||||||
|
value: imessage
|
||||||
|
effect: NoSchedule
|
||||||
|
containers:
|
||||||
|
- name: macos
|
||||||
|
image: sickcodes/docker-osx:latest@sha256:3a3c82c79bc4e73531f819ccdfa4053b3084efd7c1f645678dbf8b4b3a24369c
|
||||||
|
# QEMU needs /dev/kvm; Talos enforces `baseline` cluster-wide, so this
|
||||||
|
# only schedules because the sms namespace is labelled privileged.
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
|
env:
|
||||||
|
# Generates a unique serial / board-serial / UUID / MAC and persists
|
||||||
|
# them to bootdisk.qcow2. This synthetic identity is what iMessage
|
||||||
|
# activates against — it must stay stable across restarts, which is
|
||||||
|
# why the PVC matters.
|
||||||
|
- name: GENERATE_UNIQUE
|
||||||
|
value: "true"
|
||||||
|
# Identity is only plausible if it matches a real product line.
|
||||||
|
- name: DEVICE_MODEL
|
||||||
|
value: "iMacPro1,1"
|
||||||
|
- name: RAM
|
||||||
|
value: "12"
|
||||||
|
- name: CORES
|
||||||
|
value: "6"
|
||||||
|
- name: EXTRA
|
||||||
|
# Expose the BlueBubbles server port from the guest to the pod.
|
||||||
|
# Guest :1234 (BlueBubbles default) -> pod :1234.
|
||||||
|
value: "-device virtio-net-pci,netdev=net0 -netdev user,id=net0,hostfwd=tcp::1234-:1234"
|
||||||
|
ports:
|
||||||
|
- name: vnc
|
||||||
|
containerPort: 5999
|
||||||
|
protocol: TCP
|
||||||
|
- name: bluebubbles
|
||||||
|
containerPort: 1234
|
||||||
|
protocol: TCP
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "6"
|
||||||
|
memory: 14Gi
|
||||||
|
limits:
|
||||||
|
cpu: "12"
|
||||||
|
memory: 20Gi
|
||||||
|
volumeMounts:
|
||||||
|
- name: macos-disk
|
||||||
|
mountPath: /home/arch/OSX-KVM/disk
|
||||||
|
- name: kvm
|
||||||
|
mountPath: /dev/kvm
|
||||||
|
# No readiness probe on purpose. The guest takes many minutes to boot,
|
||||||
|
# and until macOS + BlueBubbles are installed BY HAND there is nothing
|
||||||
|
# listening on 1234. A probe here would crash-loop the pod through the
|
||||||
|
# entire interactive install.
|
||||||
|
volumes:
|
||||||
|
- name: macos-disk
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: macos-disk
|
||||||
|
- name: kvm
|
||||||
|
hostPath:
|
||||||
|
path: /dev/kvm
|
||||||
|
type: CharDevice
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: sms
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- storageclass.yaml
|
||||||
|
- pvc-macos.yaml
|
||||||
|
- deployment-macos.yaml
|
||||||
|
- service.yaml
|
||||||
|
- networkpolicy.yaml
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# iMessage delivery for the cluster.
|
||||||
|
#
|
||||||
|
# BlueBubbles' server is a macOS Electron app paired with an Objective-C helper
|
||||||
|
# that hooks Messages.app private APIs — it cannot be containerised on Linux,
|
||||||
|
# because containers share the host kernel and macOS needs XNU + Cocoa. The only
|
||||||
|
# way to run it on Talos is a full macOS VM under QEMU/KVM (Docker-OSX), which
|
||||||
|
# needs a privileged pod with /dev/kvm.
|
||||||
|
#
|
||||||
|
# Hence privileged PodSecurity: the cluster default from the Talos controlplane
|
||||||
|
# is `enforce: baseline`, which forbids privileged containers and host devices.
|
||||||
|
# Scope is limited to this namespace.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: sms
|
||||||
|
labels:
|
||||||
|
pod-security.kubernetes.io/enforce: privileged
|
||||||
|
pod-security.kubernetes.io/audit: privileged
|
||||||
|
pod-security.kubernetes.io/warn: privileged
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Default-deny. This namespace runs a privileged QEMU VM signed into an Apple
|
||||||
|
# ID and exposes an unauthenticated VNC console; nothing should reach it except
|
||||||
|
# opted-in clients.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: sms-default-deny
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/part-of: sms
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector: {}
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
sms-client: "true"
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 1234
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Persistent macOS disk image + generated hardware identity (bootdisk.qcow2).
|
||||||
|
#
|
||||||
|
# This volume is NOT disposable: it holds the VM's serial number, board serial,
|
||||||
|
# UUID and MAC, which together form the identity iMessage was activated against.
|
||||||
|
# Losing it means re-running activation, which is the least reliable step of the
|
||||||
|
# whole setup.
|
||||||
|
#
|
||||||
|
# Docker-OSX documents 128GB minimum for the guest image; 200Gi leaves room for
|
||||||
|
# the installer, the base system, and qcow2 growth.
|
||||||
|
#
|
||||||
|
# ⚠️ Single replica (see storageclass.yaml — capacity and IO both rule out 3).
|
||||||
|
# Losing worker-2's disk therefore means losing the activated identity and
|
||||||
|
# redoing iMessage activation. Once the guest is installed and activated, take
|
||||||
|
# a Longhorn snapshot/backup of this volume; that is the only redundancy here.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: macos-disk
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: longhorn-imessage-local
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 200Gi
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# VNC is how you drive the interactive macOS install. Deliberately ClusterIP —
|
||||||
|
# it is an unauthenticated console onto a machine holding a live Apple ID
|
||||||
|
# session. Reach it with `kubectl port-forward`, never an Ingress.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: macos-vnc
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: macos-bluebubbles
|
||||||
|
ports:
|
||||||
|
- name: vnc
|
||||||
|
port: 5999
|
||||||
|
targetPort: vnc
|
||||||
|
---
|
||||||
|
# The BlueBubbles REST API, once installed inside the guest. This is the stable
|
||||||
|
# name cluster services use, so callers never depend on the pod IP or on whether
|
||||||
|
# the backend is this VM or a real Mac mini later.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: bluebubbles
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: macos-bluebubbles
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 1234
|
||||||
|
targetPort: bluebubbles
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Dedicated StorageClass for the macOS VM disk.
|
||||||
|
#
|
||||||
|
# The default `longhorn` class does not work here, for two independent reasons:
|
||||||
|
#
|
||||||
|
# 1. Replica count. Default is 3, and Longhorn schedules against
|
||||||
|
# storageMaximum - storageScheduled with over-provisioning at 100%. Free
|
||||||
|
# space is cp-1 146Gi / cp-2 8Gi / cp-3 146Gi / worker-1 292Gi, so a 200Gi
|
||||||
|
# volume has only one node that can hold even a single replica — a 3-replica
|
||||||
|
# volume fails outright with ReplicaSchedulingFailure.
|
||||||
|
# 2. Binding mode. `Immediate` provisions the volume the moment the PVC is
|
||||||
|
# created, before any pod is scheduled. Combined with strict-local that
|
||||||
|
# pins the data to an arbitrary node, not the one the VM runs on.
|
||||||
|
#
|
||||||
|
# So: one replica, kept local to the VM, bound only once the pod has a node.
|
||||||
|
apiVersion: storage.k8s.io/v1
|
||||||
|
kind: StorageClass
|
||||||
|
metadata:
|
||||||
|
name: longhorn-imessage-local
|
||||||
|
provisioner: driver.longhorn.io
|
||||||
|
allowVolumeExpansion: true
|
||||||
|
reclaimPolicy: Delete
|
||||||
|
# The pod is pinned to worker-2 by nodeSelector; wait for it to be scheduled so
|
||||||
|
# the replica is placed on that node and not somewhere else.
|
||||||
|
volumeBindingMode: WaitForFirstConsumer
|
||||||
|
parameters:
|
||||||
|
# A qcow2 backing a live VM is latency-sensitive and rewritten constantly.
|
||||||
|
# Serving it over the network from another node's disk would be the single
|
||||||
|
# worst thing for guest responsiveness, so force it local.
|
||||||
|
numberOfReplicas: "1"
|
||||||
|
dataLocality: "strict-local"
|
||||||
|
staleReplicaTimeout: "30"
|
||||||
|
fsType: "ext4"
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
temporal:
|
|
||||||
oidc_client_id: ENC[AES256_GCM,data:y6eSNJpCNCg=,iv:4n5fIeiVG2JZOCD4yZ4Asm/gWvRl0QNFiRGTR1bs8ws=,tag:liLSr9sadR82UPq6d3gqGA==,type:str]
|
|
||||||
oidc_client_secret: ENC[AES256_GCM,data:ebk0FVtSfq7JbIT+84cigdLg8zfN+4HTfGxlgqLw6hs1H92SFRHQFXO1l3U=,iv:qxa8aqUVoN31PpzAhs6bNMDCM6yPvOT27urIrM1rfo0=,tag:Vo25X8Znw7eh5DNq1eIJEw==,type:str]
|
|
||||||
sops:
|
|
||||||
age:
|
|
||||||
- enc: |
|
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArZ1pyMXZ0Y3RFQVZkcTg5
|
|
||||||
aENyemRueXpIM0FaUVhMc2ZyVDRyWWlzK1JzCjA0OTZ0aGJDNVVSM29CY1RzQWJL
|
|
||||||
eVNiSCtMSXdmbEtKeGt1L3NYMy9ibUEKLS0tIGJnU3A4MjVnZ1pCa1lPMnVoM0xo
|
|
||||||
dWs1cVM5ZCszbXp6eFltRVhGbFc0ajQKyCc8lClnSqWUxhNOr1FDCwn5V7nvjxPN
|
|
||||||
7kjQpldseaRbsy+TM5sFQ1w6Acmun9uYjzs8PtmTNaayc/AFfspufA==
|
|
||||||
-----END AGE ENCRYPTED FILE-----
|
|
||||||
recipient: age1smu533f803gmd0jq60s2zaj9zlznajy0ca6rtewd4r37mr2hs3uqsrldfh
|
|
||||||
lastmodified: "2026-07-15T23:22:55Z"
|
|
||||||
mac: ENC[AES256_GCM,data:0YuRxQHfdeKbjJzxHI4iBc7I2KnalNJmybB6bVQxvdRutsxKXW/bu99yqHkD4y/nI66Pi6d7LhrPhhGat3x2c6Drs7QRYVWnZ75thil6yWsOcpH7H1esxEWTP3cpoMzGdS0aF6zwU9H+WDvfm/t1wpeDpJly3eczcPvfbK1joDU=,iv:TgwfpI+ZVLyd2c/PKWsJbCI9yKkADpPPafJ5No0fLks=,tag:nqwQHtZ263CI+fY9iTUAzQ==,type:str]
|
|
||||||
unencrypted_suffix: _unencrypted
|
|
||||||
version: 3.13.2
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# Wave 0 — Nginx Ingress Controller
|
|
||||||
# Foundational infrastructure required for all ingress resources and ArgoCD UI access.
|
|
||||||
# Must be wave 0 to ensure LoadBalancer IP is available before other apps deploy.
|
|
||||||
---
|
|
||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Application
|
|
||||||
metadata:
|
|
||||||
name: ingress-nginx
|
|
||||||
namespace: argocd
|
|
||||||
annotations:
|
|
||||||
argocd.argoproj.io/sync-wave: "0"
|
|
||||||
finalizers:
|
|
||||||
- resources-finalizer.argocd.argoproj.io
|
|
||||||
spec:
|
|
||||||
project: homelab
|
|
||||||
revisionHistoryLimit: 3
|
|
||||||
sources:
|
|
||||||
- repoURL: https://kubernetes.github.io/ingress-nginx
|
|
||||||
chart: ingress-nginx
|
|
||||||
targetRevision: "4.15.1"
|
|
||||||
helm:
|
|
||||||
valueFiles:
|
|
||||||
- $values/k8s/bootstrap/ingress/nginx-values.yaml
|
|
||||||
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
|
||||||
targetRevision: main
|
|
||||||
ref: values
|
|
||||||
destination:
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
namespace: ingress-nginx
|
|
||||||
syncPolicy:
|
|
||||||
automated:
|
|
||||||
prune: true
|
|
||||||
selfHeal: true
|
|
||||||
syncOptions:
|
|
||||||
- CreateNamespace=true
|
|
||||||
- ServerSideApply=true
|
|
||||||
retry:
|
|
||||||
limit: 3
|
|
||||||
backoff:
|
|
||||||
duration: 10s
|
|
||||||
factor: 2
|
|
||||||
maxDuration: 3m
|
|
||||||
@@ -17,10 +17,12 @@ spec:
|
|||||||
syncOptions:
|
syncOptions:
|
||||||
- CreateNamespace=true
|
- CreateNamespace=true
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: .
|
# ksops decrypts every *.enc.yaml here at kustomize-build time (repo-server
|
||||||
plugin:
|
# runs `kustomize build --enable-alpha-plugins --enable-exec`). Replaces the
|
||||||
name: sops-secrets-v1.0
|
# old sops-secrets-v1.0 CMP whose discover glob silently hijacked kustomize
|
||||||
|
# rendering of any app whose path contained a *.enc.yaml.
|
||||||
|
path: k8s/argocd/secrets
|
||||||
destination:
|
destination:
|
||||||
server: https://kubernetes.default.svc
|
server: https://kubernetes.default.svc
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ spec:
|
|||||||
helm:
|
helm:
|
||||||
valueFiles:
|
valueFiles:
|
||||||
- $values/k8s/bootstrap/cert-manager/cert-manager-values.yaml
|
- $values/k8s/bootstrap/cert-manager/cert-manager-values.yaml
|
||||||
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
ref: values
|
ref: values
|
||||||
destination:
|
destination:
|
||||||
@@ -57,6 +57,13 @@ spec:
|
|||||||
helm:
|
helm:
|
||||||
values: |
|
values: |
|
||||||
reloader:
|
reloader:
|
||||||
|
# Watch every workload — no per-Deployment reloader annotation needed
|
||||||
|
# (several charts, e.g. homarr, don't expose Deployment-level
|
||||||
|
# annotations). reloadOnCreate rolls a workload when a Secret/ConfigMap
|
||||||
|
# it references is first CREATED, not only updated — so ksops-delivered
|
||||||
|
# secrets landing after a pod started auto-restart it.
|
||||||
|
autoReloadAll: true
|
||||||
|
reloadOnCreate: true
|
||||||
deployment:
|
deployment:
|
||||||
tolerations:
|
tolerations:
|
||||||
- key: node-role.kubernetes.io/control-plane
|
- key: node-role.kubernetes.io/control-plane
|
||||||
@@ -86,11 +93,14 @@ spec:
|
|||||||
project: homelab
|
project: homelab
|
||||||
revisionHistoryLimit: 3
|
revisionHistoryLimit: 3
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
|
# A real kustomization.yaml (resources: the 3 issuer/CA files) renders these
|
||||||
|
# deterministically. The previous directory.include with bare filenames
|
||||||
|
# rendered EMPTY — ArgoCD's include glob never matched — so this app silently
|
||||||
|
# tracked 0 resources; its ConfigMaps/Issuers only existed from bootstrap
|
||||||
|
# kubectl apply, and an automated prune wiped them.
|
||||||
path: k8s/bootstrap/cert-manager
|
path: k8s/bootstrap/cert-manager
|
||||||
directory:
|
|
||||||
include: "letsencrypt-issuer.yaml"
|
|
||||||
destination:
|
destination:
|
||||||
server: https://kubernetes.default.svc
|
server: https://kubernetes.default.svc
|
||||||
namespace: cert-manager
|
namespace: cert-manager
|
||||||
@@ -98,6 +108,8 @@ spec:
|
|||||||
automated:
|
automated:
|
||||||
prune: true
|
prune: true
|
||||||
selfHeal: true
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
---
|
---
|
||||||
# Consolidated: wildcard-cert + homelab-ingress → ingress-config
|
# Consolidated: wildcard-cert + homelab-ingress → ingress-config
|
||||||
# Manages both the wildcard TLS certificate and all Ingress rules.
|
# Manages both the wildcard TLS certificate and all Ingress rules.
|
||||||
@@ -115,7 +127,7 @@ spec:
|
|||||||
project: homelab
|
project: homelab
|
||||||
revisionHistoryLimit: 3
|
revisionHistoryLimit: 3
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: k8s/bootstrap/ingress
|
path: k8s/bootstrap/ingress
|
||||||
destination:
|
destination:
|
||||||
|
|||||||
@@ -1,43 +1,8 @@
|
|||||||
# Wave 0 — networking policies layered on the Cilium CNI + CoreDNS that the
|
# Wave 0 — networking substrate is Talos-owned (terraform inlineManifests), not
|
||||||
# cluster bootstrap already installed (substrate). These are raw manifests only.
|
# ArgoCD:
|
||||||
apiVersion: argoproj.io/v1alpha1
|
# - CoreDNS Corefile + hostname rewrites -> terraform/files/coredns/Corefile
|
||||||
kind: Application
|
# - Cilium LB-IPAM pool + L2 announcement -> terraform/files/cilium/*.yaml
|
||||||
metadata:
|
# Both were previously ArgoCD apps here whose empty `resources: []`
|
||||||
name: cilium-policy
|
# kustomizations never actually applied them (live objects came from manual
|
||||||
namespace: argocd
|
# kubectl). Managing them from ArgoCD too would let two reconcilers fight. This
|
||||||
annotations:
|
# file intentionally defines no Applications now.
|
||||||
argocd.argoproj.io/sync-wave: "0"
|
|
||||||
spec:
|
|
||||||
project: homelab
|
|
||||||
source:
|
|
||||||
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
|
||||||
targetRevision: main
|
|
||||||
path: k8s/bootstrap/cilium
|
|
||||||
destination:
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
namespace: kube-system
|
|
||||||
syncPolicy:
|
|
||||||
automated:
|
|
||||||
prune: true
|
|
||||||
selfHeal: true
|
|
||||||
---
|
|
||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Application
|
|
||||||
metadata:
|
|
||||||
name: coredns-config
|
|
||||||
namespace: argocd
|
|
||||||
annotations:
|
|
||||||
argocd.argoproj.io/sync-wave: "0"
|
|
||||||
spec:
|
|
||||||
project: homelab
|
|
||||||
source:
|
|
||||||
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
|
||||||
targetRevision: main
|
|
||||||
path: k8s/bootstrap/coredns
|
|
||||||
destination:
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
namespace: kube-system
|
|
||||||
syncPolicy:
|
|
||||||
automated:
|
|
||||||
prune: true
|
|
||||||
selfHeal: true
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ spec:
|
|||||||
helm:
|
helm:
|
||||||
valueFiles:
|
valueFiles:
|
||||||
- $values/k8s/infra/minio/minio-operator-values.yaml
|
- $values/k8s/infra/minio/minio-operator-values.yaml
|
||||||
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
ref: values
|
ref: values
|
||||||
destination:
|
destination:
|
||||||
@@ -41,7 +41,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: homelab
|
project: homelab
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: k8s/infra/minio
|
path: k8s/infra/minio
|
||||||
destination:
|
destination:
|
||||||
@@ -66,12 +66,20 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: homelab
|
project: homelab
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: k8s/infra/longhorn
|
path: k8s/infra/longhorn
|
||||||
destination:
|
destination:
|
||||||
server: https://kubernetes.default.svc
|
server: https://kubernetes.default.svc
|
||||||
namespace: longhorn-system
|
namespace: longhorn-system
|
||||||
|
# Longhorn writes disk state back into its own Node CRs — the disk key it
|
||||||
|
# generates, storageReserved, diskType, evictionRequested. Git declares only
|
||||||
|
# allowScheduling; without this the controller's writes read as drift forever.
|
||||||
|
ignoreDifferences:
|
||||||
|
- group: longhorn.io
|
||||||
|
kind: Node
|
||||||
|
jsonPointers:
|
||||||
|
- /spec/disks
|
||||||
syncPolicy:
|
syncPolicy:
|
||||||
automated:
|
automated:
|
||||||
prune: true
|
prune: true
|
||||||
@@ -94,7 +102,7 @@ spec:
|
|||||||
skipCrds: true
|
skipCrds: true
|
||||||
valueFiles:
|
valueFiles:
|
||||||
- $values/k8s/infra/monitoring/prometheus-values.yaml
|
- $values/k8s/infra/monitoring/prometheus-values.yaml
|
||||||
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
ref: values
|
ref: values
|
||||||
destination:
|
destination:
|
||||||
@@ -144,7 +152,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: homelab
|
project: homelab
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: k8s/infra/monitoring/crds
|
path: k8s/infra/monitoring/crds
|
||||||
destination:
|
destination:
|
||||||
@@ -175,7 +183,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: homelab
|
project: homelab
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: k8s/infra/monitoring
|
path: k8s/infra/monitoring
|
||||||
destination:
|
destination:
|
||||||
@@ -205,7 +213,7 @@ spec:
|
|||||||
helm:
|
helm:
|
||||||
valueFiles:
|
valueFiles:
|
||||||
- $values/k8s/infra/monitoring/blackbox-exporter-values.yaml
|
- $values/k8s/infra/monitoring/blackbox-exporter-values.yaml
|
||||||
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
ref: values
|
ref: values
|
||||||
destination:
|
destination:
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ spec:
|
|||||||
helm:
|
helm:
|
||||||
valueFiles:
|
valueFiles:
|
||||||
- $values/k8s/infra/logging/loki-values.yaml
|
- $values/k8s/infra/logging/loki-values.yaml
|
||||||
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
ref: values
|
ref: values
|
||||||
destination:
|
destination:
|
||||||
@@ -53,7 +53,7 @@ spec:
|
|||||||
helm:
|
helm:
|
||||||
valueFiles:
|
valueFiles:
|
||||||
- $values/k8s/infra/logging/grafana-values.yaml
|
- $values/k8s/infra/logging/grafana-values.yaml
|
||||||
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
ref: values
|
ref: values
|
||||||
destination:
|
destination:
|
||||||
@@ -87,7 +87,7 @@ spec:
|
|||||||
helm:
|
helm:
|
||||||
valueFiles:
|
valueFiles:
|
||||||
- $values/k8s/infra/logging/promtail-values.yaml
|
- $values/k8s/infra/logging/promtail-values.yaml
|
||||||
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
ref: values
|
ref: values
|
||||||
destination:
|
destination:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ spec:
|
|||||||
helm:
|
helm:
|
||||||
valueFiles:
|
valueFiles:
|
||||||
- $values/k8s/infra/iam/vault-values.yaml
|
- $values/k8s/infra/iam/vault-values.yaml
|
||||||
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
ref: values
|
ref: values
|
||||||
destination:
|
destination:
|
||||||
@@ -46,7 +46,7 @@ spec:
|
|||||||
helm:
|
helm:
|
||||||
valueFiles:
|
valueFiles:
|
||||||
- $values/k8s/infra/iam/authentik-values.yaml
|
- $values/k8s/infra/iam/authentik-values.yaml
|
||||||
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
ref: values
|
ref: values
|
||||||
destination:
|
destination:
|
||||||
@@ -68,7 +68,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: homelab
|
project: homelab
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: k8s/infra/iam
|
path: k8s/infra/iam
|
||||||
destination:
|
destination:
|
||||||
@@ -90,7 +90,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: homelab
|
project: homelab
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: k8s/infra/forgejo-runner
|
path: k8s/infra/forgejo-runner
|
||||||
destination:
|
destination:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: homelab
|
project: homelab
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: k8s/infra/databases
|
path: k8s/infra/databases
|
||||||
destination:
|
destination:
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: homelab
|
project: homelab
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: k8s/apps/messaging/kafka-cluster
|
path: k8s/apps/messaging/kafka-cluster
|
||||||
destination:
|
destination:
|
||||||
@@ -89,7 +89,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: homelab
|
project: homelab
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: k8s/apps/messaging/queue-crd
|
path: k8s/apps/messaging/queue-crd
|
||||||
destination:
|
destination:
|
||||||
@@ -110,7 +110,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: homelab
|
project: homelab
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: k8s/apps/messaging/management-service
|
path: k8s/apps/messaging/management-service
|
||||||
destination:
|
destination:
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Wave 7 — Kong, the cluster's internal API gateway (namespace `api`).
|
||||||
|
#
|
||||||
|
# Sits between nginx and the backend services: nginx owns the edge and TLS,
|
||||||
|
# Kong owns routing policy, auth and rate limiting. Wave 7 puts it after the
|
||||||
|
# data/messaging tiers it fronts and before the wave-8 applications that
|
||||||
|
# publish routes into it.
|
||||||
|
#
|
||||||
|
# DB-less: routing config comes from Kubernetes objects (Ingress with
|
||||||
|
# `ingressClassName: kong`, plus KongPlugin/KongConsumer CRDs), so git remains
|
||||||
|
# the source of truth and there are no migration Jobs on upgrade.
|
||||||
|
#
|
||||||
|
# CRDs ship in the chart's crds/ directory; ArgoCD applies those by default
|
||||||
|
# (helm.skipCrds is left false).
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: kong
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "7"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
revisionHistoryLimit: 3
|
||||||
|
sources:
|
||||||
|
- repoURL: https://charts.konghq.com
|
||||||
|
chart: kong
|
||||||
|
targetRevision: "3.4.1"
|
||||||
|
helm:
|
||||||
|
valueFiles:
|
||||||
|
- $values/k8s/apps/api/kong-values.yaml
|
||||||
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
ref: values
|
||||||
|
# The nginx Ingress for api.riotpiao.com. Kept in this Application rather
|
||||||
|
# than the central k8s/bootstrap/ingress/ingress.yaml because that one syncs
|
||||||
|
# in wave 1, before namespace `api` exists.
|
||||||
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/apps/api
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: api
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
# The chart's CRDs exceed the annotation size limit that client-side
|
||||||
|
# apply relies on; server-side apply avoids the
|
||||||
|
# "metadata.annotations: Too long" failure CRDs commonly hit.
|
||||||
|
- ServerSideApply=true
|
||||||
|
retry:
|
||||||
|
limit: 3
|
||||||
|
backoff:
|
||||||
|
duration: 10s
|
||||||
|
factor: 2
|
||||||
|
maxDuration: 3m
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Wave 6 — the model servers behind api.riotpiao.com (namespace `llm-serving`).
|
||||||
|
#
|
||||||
|
# Syncs before wave 7 (Kong), so the predictor Services exist before the routes
|
||||||
|
# that point at them. KServe itself is part of the substrate; this Application
|
||||||
|
# owns only the InferenceServices.
|
||||||
|
#
|
||||||
|
# Adopted from live state on 2026-08-15. These five had been `kubectl apply`-ed
|
||||||
|
# by hand — no ArgoCD ownership, present in no repo — so every change to them
|
||||||
|
# was drift by definition. Each manifest was exported from the cluster and
|
||||||
|
# verified with `kubectl diff` returning empty before this file existed; the
|
||||||
|
# first sync therefore adopted them without restarting anything.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: llm-serving
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "6"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
revisionHistoryLimit: 3
|
||||||
|
source:
|
||||||
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/apps/llm-serving
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: llm-serving
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
# `prune: false` here, unlike every other Application in this repo, and it
|
||||||
|
# is not an oversight.
|
||||||
|
#
|
||||||
|
# ArgoCD tracks ownership with the `argocd.argoproj.io/instance` label
|
||||||
|
# (argocd-cm `application.instanceLabelKey`). KServe copies an
|
||||||
|
# InferenceService's labels onto the Deployment and Service it generates —
|
||||||
|
# visible today as `app.kubernetes.io/name` and `part-of` on
|
||||||
|
# `ornith-predictor`. So once ArgoCD labels an InferenceService, KServe
|
||||||
|
# propagates that tracking label to children that are not in git, ArgoCD
|
||||||
|
# reads them as extraneous, prunes them, and KServe recreates them. That
|
||||||
|
# loop churns GPU pods.
|
||||||
|
#
|
||||||
|
# Deleting an InferenceService therefore means deleting the file AND
|
||||||
|
# removing the object, rather than relying on prune.
|
||||||
|
prune: false
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
# KServe CRDs are large; server-side apply avoids the
|
||||||
|
# "metadata.annotations: Too long" failure client-side apply hits, and is
|
||||||
|
# the correct mode for adopting objects an operator also writes to.
|
||||||
|
- ServerSideApply=true
|
||||||
|
retry:
|
||||||
|
limit: 3
|
||||||
|
backoff:
|
||||||
|
duration: 10s
|
||||||
|
factor: 2
|
||||||
|
maxDuration: 3m
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
# Wave 8 — end-user workloads: Temporal, Portainer, and the two network
|
# Wave 8 — end-user workloads: Temporal, Portainer, and the cloudflared tunnel.
|
||||||
# helpers (cloudflared tunnel, duckdns updater) that are already running.
|
|
||||||
# Experimental dirs (llm, forge, dev-tools, shadowsocks) are intentionally
|
# Experimental dirs (llm, forge, dev-tools, shadowsocks) are intentionally
|
||||||
# NOT included yet — add them here once they're production-ready.
|
# NOT included yet — add them here once they're production-ready.
|
||||||
# temporal using unified CNPG pattern (app user, temporal-db-app secret)
|
# temporal using unified CNPG pattern (app user, temporal-db-app secret)
|
||||||
@@ -20,7 +19,7 @@ spec:
|
|||||||
helm:
|
helm:
|
||||||
valueFiles:
|
valueFiles:
|
||||||
- $values/k8s/apps/temporal/temporal-values.yaml
|
- $values/k8s/apps/temporal/temporal-values.yaml
|
||||||
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
ref: values
|
ref: values
|
||||||
destination:
|
destination:
|
||||||
@@ -49,7 +48,7 @@ spec:
|
|||||||
helm:
|
helm:
|
||||||
valueFiles:
|
valueFiles:
|
||||||
- $values/k8s/apps/portainer/portainer-values.yaml
|
- $values/k8s/apps/portainer/portainer-values.yaml
|
||||||
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
ref: values
|
ref: values
|
||||||
destination:
|
destination:
|
||||||
@@ -72,7 +71,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: homelab
|
project: homelab
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: k8s/apps/cloudflared
|
path: k8s/apps/cloudflared
|
||||||
destination:
|
destination:
|
||||||
@@ -88,19 +87,19 @@ spec:
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
apiVersion: argoproj.io/v1alpha1
|
||||||
kind: Application
|
kind: Application
|
||||||
metadata:
|
metadata:
|
||||||
name: duckdns
|
name: agent-pod
|
||||||
namespace: argocd
|
namespace: argocd
|
||||||
annotations:
|
annotations:
|
||||||
argocd.argoproj.io/sync-wave: "8"
|
argocd.argoproj.io/sync-wave: "8"
|
||||||
spec:
|
spec:
|
||||||
project: homelab
|
project: homelab
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: k8s/apps/duckdns
|
path: k8s/apps/agent-pod
|
||||||
destination:
|
destination:
|
||||||
server: https://kubernetes.default.svc
|
server: https://kubernetes.default.svc
|
||||||
namespace: duckdns
|
namespace: agent-pod
|
||||||
syncPolicy:
|
syncPolicy:
|
||||||
automated:
|
automated:
|
||||||
prune: true
|
prune: true
|
||||||
@@ -108,6 +107,37 @@ spec:
|
|||||||
syncOptions:
|
syncOptions:
|
||||||
- CreateNamespace=true
|
- CreateNamespace=true
|
||||||
---
|
---
|
||||||
|
# iMessage/SMS delivery. Raw manifests: a privileged macOS VM (Docker-OSX)
|
||||||
|
# running the BlueBubbles server, plus its dedicated local StorageClass.
|
||||||
|
#
|
||||||
|
# Pinned to worker-2 via nodeSelector `workload: imessage` + a matching
|
||||||
|
# toleration for that node's taint. Until worker-2 is provisioned this app
|
||||||
|
# syncs everything except the pod, which stays Pending — that is expected.
|
||||||
|
#
|
||||||
|
# No CreateNamespace: namespace.yaml carries `pod-security: privileged`, which
|
||||||
|
# the VM needs (/dev/kvm, privileged), and an ArgoCD-created namespace would
|
||||||
|
# not have those labels.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: sms
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "8"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/apps/sms
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: sms
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
---
|
||||||
# Consolidated: homarr + homarr-patches → homarr
|
# Consolidated: homarr + homarr-patches → homarr
|
||||||
# Helm chart + values + PostSync hook patch (fix-probes-job.yaml)
|
# Helm chart + values + PostSync hook patch (fix-probes-job.yaml)
|
||||||
apiVersion: argoproj.io/v1alpha1
|
apiVersion: argoproj.io/v1alpha1
|
||||||
@@ -126,10 +156,10 @@ spec:
|
|||||||
helm:
|
helm:
|
||||||
valueFiles:
|
valueFiles:
|
||||||
- $values/k8s/apps/homarr/homarr-values.yaml
|
- $values/k8s/apps/homarr/homarr-values.yaml
|
||||||
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
ref: values
|
ref: values
|
||||||
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: k8s/apps/homarr # PostSync hook: fix-probes-job.yaml
|
path: k8s/apps/homarr # PostSync hook: fix-probes-job.yaml
|
||||||
destination:
|
destination:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
description: Homelab GitOps — single-repo, in-cluster destinations only
|
description: Homelab GitOps — single-repo, in-cluster destinations only
|
||||||
sourceRepos:
|
sourceRepos:
|
||||||
- https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
- https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
# Public Helm chart repos referenced by k8s/argocd/apps/* and bootstrap/*
|
# Public Helm chart repos referenced by k8s/argocd/apps/* and bootstrap/*
|
||||||
- https://cloudnative-pg.github.io/charts
|
- https://cloudnative-pg.github.io/charts
|
||||||
- https://dl.gitea.com/charts/
|
- https://dl.gitea.com/charts/
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: homelab
|
project: homelab
|
||||||
source:
|
source:
|
||||||
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: k8s/argocd/apps
|
path: k8s/argocd/apps
|
||||||
directory:
|
directory:
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
cloudflared:
|
||||||
|
tunnelToken: ENC[AES256_GCM,data:mFqJUuK8bzKnPAk4ZwBSLP7p6IiUCldmFNKSS5P75rKBgrrTrRB24Vqf6W7UwM2w+/CNzkQ0QByNw4/TzjHDgNH1UJAYXlVvm4H8/NPp4WPWlv5YO67l/mAmPVnBEmJzU0KgivCQm3psEYiEfbgiIbujqr0isgtkU856BimYHqtEdBmYRuJ5c73noaV6NWB1aTnEiCOmmNs+NCkN2GswgSi/zJLbd0HJPQT8h8+3rI8L7nC0/HJvYw==,iv:O3i0v+M5L3i9O7SbBtDAJe5IsQDgw+alI0Y9arZNojs=,tag:Tl9f0oVsBi68AyK3QwlHfg==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBWlk3V1d1cjFXV3VrMUVN
|
||||||
|
LzA3NlVKbllHYkdEb2pESEJkQ2RyU3UzOVZnCk1NdGVUSVpHMHNYUGhOdEhoTi81
|
||||||
|
YVlvU2Q2WmRUdC9KTGlBaUVnU01rbzgKLS0tIEhLNzl2UmpwYjQwNTl1YllnQ09X
|
||||||
|
Y0ZwR1N3Y2VNQ3VkbkJvRG56RWE3UWMK5tv5dgjKlbHq1Rh4NC0+3b9n1yTE/7vW
|
||||||
|
TehGG7k18zclBiZD2y5l4/CeDPM/yi5kfbPuxG1kURffw1xUeac+Ig==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:DshqXuhhJo0eLMTJ+HeAEk6JOtzGQ6OyUd7fSIGWHL/rGfDPliDBdTqpWw1899kgF0LRxY87rTTod65KvINp2HHc8KBQKa5MiAoTXQBThlFYIHFDOL2XQve9p6TKzsVDsQrBsl0lGt9V+Ou+YWUGh9kx63me1wAl8aZ9ltwWvZ0=,iv:4exYbWC7f7WGUncT1KcjrTy0r8Ox3YCctShyMiiaw+4=,tag:YMYOvwm2844LC76n3QvEdQ==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
temporal:
|
||||||
|
oidc_client_id: ENC[AES256_GCM,data:gyzO+wMlDrg=,iv:4n5fIeiVG2JZOCD4yZ4Asm/gWvRl0QNFiRGTR1bs8ws=,tag:MV1b/F5OOihe+K5qlkZnTA==,type:str]
|
||||||
|
oidc_client_secret: ENC[AES256_GCM,data:ND7XhSln6AVq6Qy465M6x5Hupjp+xZQKnqT+OW1U+bR7CAmsnZNom1wVpm0=,iv:qxa8aqUVoN31PpzAhs6bNMDCM6yPvOT27urIrM1rfo0=,tag:SRoQ7q7Nqn8TZGxmLxKXKg==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1MEVQSkE2WmpOanp2cXY4
|
||||||
|
M3NVNmd0U056bE52dXk5YUNBcE04c0V2SW04Cm1KTDIyc08xRHR4bnU2N1ptbHFM
|
||||||
|
UXRuOVQzaU1vQStZMG9yRzYyd09Idm8KLS0tIERTNjl1Q1ZtTDlGcGFpdlAwVjhQ
|
||||||
|
U0VxMTJBRkc1bGlXZkFxZDQ4OWlwYlEKF35/ZOxDMvxhXV4rIhtNdaE7+vQ9JOs4
|
||||||
|
+ou88d2WnLD9U50fa1RC3+zS/5CwzIAOetfHeVSjPLkO9oVXuGhU3g==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:Gf9u8t515vhogURw7sOb5cAMKeEBEzuX3V6mDP4OtT6bPinyLRwvgp/j5E5lB0ANYggFHXiv4FL8Nb/pAQXxNe0v8rxwuWiTSspJrM2l2JzZ/nLDAfQiyOVUsoYY15HsGzjbmRT5VEQf/9ceSVqS2tAliZj7hH/hDsncc46vWHI=,iv:gOuSf898MMtzW6L2K+Z/yObn+9Bg42JvT0IlhGK+HSs=,tag:2MEPiaSqdedUibMuO/2RSg==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:zdc=,iv:VvjvrS5PVNAMIaOE0LaWU+tHcUIYVQDnCANQz6myktY=,tag:xGyWDhRCwwiNny7hPllf5g==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:2bf5Zfy8,iv:5Oz423GzUWmgdaaZHbrtedwRHIAPIuLh4iDMieLL05s=,tag:GNW1ROjlzGvO/4tIOSuH3Q==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:7RO0Qxkc+/sA,iv:wKe9A8d7QJSx/6rlEY5H6lU8V24TJqr5IpXQCBc8QgM=,tag:iF6z1MNEXG3pCz2cQ18gLg==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:RcduxHHLtjH9,iv:opOVx1lL2ltDqQgsleN7NdMAq0TyFr/YQO3FFsHh5AA=,tag:fIwO31apqIatRRzBvamw6g==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:C+JjyZt5,iv:xs49Lz6zRzcf3spiPzdUKTm2HZ+VgFahN6wjIe81JI4=,tag:VllJ54MqU+klA8xAIebjrA==,type:str]
|
||||||
|
stringData:
|
||||||
|
models.json: ENC[AES256_GCM,data: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,iv:8Z39eWukGSMePh/3Dj35e6Zahejil+eeMSqwMYf3snI=,tag:FyBg1y0IQEg/m0mgpf1ESg==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBZzB5REh6amhCNzRmY0ta
|
||||||
|
MkN3Y0ZlR29lQ3h2SWo3cW5CUThkL2RnU1NVCmt2ZkhIZTlHN1RQRkFrTjVvbjVw
|
||||||
|
RGRrTXRoYmdQcnlMSEo3ZWsrZUQ5cHMKLS0tIGU2TGJqZDRxUGJpZzRveEtZankx
|
||||||
|
MzhrT1R2akxxby9QVzd1RXB0RDY1LzQKOF+/e5z5lPX6Y1sMTAHuDj3YqW1m+sBd
|
||||||
|
u/0R0YnBonYM3wS5nJE3NZMkImaAdQlUjOzQepfBldG+lz++rlnAww==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-18T20:06:08Z"
|
||||||
|
mac: ENC[AES256_GCM,data:IM9HkpdwtQE2wCkjwDWOmHH4uP7TlIsrK4TVytiecvYz4SiLk6IRUSIu7I3a+F+dtltC2WtokoATaB69DTXPoI54amzzptirxiFD5FbaU+u2gLjo7KI7V0smYGuKqMYwnod2L/4GdlvP6xjVxFWuA01rQRaYBkFSumS4NABl/I4=,iv:al1MyBmFwni8gap7PPZxWaCwqFKCicfPq6nVmrSn9Xc=,tag:KJGTDQ8xRGF8qMOjoQ+dng==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:seY=,iv:1q2bg1QBNDDQPYNu1S08MZ+Ix8WLthxObsrqTMJEVhY=,tag:OMh/stn4vVScOEAyviuurw==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:QwqgPMrm,iv:tv6rUcezEgQigvHVw05Mevt6BHDD5AARuoUmQpry3AU=,tag:m9RLNezJsLYGSRWnozfkoQ==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:ZJtohnEDN2NBFrEhhvGuw4s=,iv:SU5LuA3Lqjh+q7nVxeVVtzeXiOxxVserNno5c7b47lM=,tag:YLprEk7oVjVsIDVANWDPrQ==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:LieEC+YWJzq+,iv:U00VXejzlpgOYS7Yxix/MnEW0PEvmwOuOXMUi2CWIps=,tag:QvFYZIexWhyvBYTruGMifQ==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:U41EbP+H,iv:CKDqec0jfVtbOtEyqQpV9CHJNCwhneYi0wqK8QU2vnY=,tag:CSJ04pkPslevQrpSjhXNHw==,type:str]
|
||||||
|
stringData:
|
||||||
|
id_forgejo: ENC[AES256_GCM,data:dgwezImUFb2r6MMZ2V1ZMuy32DAVZ9YMzlr9zYlXKEuzeCaWn29DuqgeP5PJQFFKwd37Y9jmzXwuKD5KrC9WCAzrS2VClxmJYHj2Bf842qtKa4x3SubHu4w6wp8v42wQYCU0Kygc5UgkCFYgOBgvpYCFKhiro6Mg/PBTWTIIgrLlSVwUWPIcaX6Sq4ZW4sy6CZ0H/pEMDAjU8drZ4KmGw7W4FshNynplMuP9ewbSs9yxtuAsKt4ezuUQBGHmAYdPHytC9HUzf67z/j6TF/zg6XgTe0AG116cNddAL8oNermCIkkKuTwb5xy1+3WRjjacC0K5Ire1fV5RyXtD/1JgEHJ8gbPK1w1V5fHZXX/ARvQFlP+uqxFYmPkCHBmK/R6/VvAghI/KYHlc76hdsAe0sd1UrPIHjVEtwgvDQl+4de9L9cHG3Qxj23ajlpISWA3tGADhwzXDxi0KYClckebrCKhizWFQLYXezALYklgY2yghG9jZHufV9jRjYSr9DLtwUmVf3oglAzKkWErNs8dWqEKYFx7HpFf0FiZh,iv:1aX4E+R1oFMm9eI1RENFIYCoatX8BdHo6PhrEGq3rtg=,tag:nwniLOOmpYd3Z6R92SvB3Q==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvRmtrdkpMOVhRdTVucVlr
|
||||||
|
QmlENGdDN2J1MUczSmRhNUpwYlBjWXhqbERzCnpLRFl1bVVtSFE3L1l4T3Rwd2ZQ
|
||||||
|
Z1MxTzJYZWovMmd2RTA3RFJVdVRRTEUKLS0tIDFQWHh2dmQxU1FUZnUxalhTdEd3
|
||||||
|
c3dUVzg2L2VOWFlmUG9XdFNWN2RvQkkKYqSmFMkDV/T7AOjKYQNJW85gUzFraRre
|
||||||
|
GhwAuPJ9oNQAhSRa5z2p6ghoUplSXtNZ6H2OzETRfOc4N2cHLljW/Q==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-18T04:18:28Z"
|
||||||
|
mac: ENC[AES256_GCM,data:ScGMBQqGm31fWDZurmlmk+rnlvyaQkalUcJ6ds/U5JJ2niTf7Llbruuqt2EzMQO09fT2kQqAIYr4cF61Czip6rDhpG8I3H3lBc2mOqUXbeCFyRL6mRpVYPOgkSzgyfXUfe8T++54UhQ0Clv/8Z4XB48oTs9detkUDzAfxP4azRE=,iv:edhsVviyjAm48Blm5CEQ0PBhcWVX97xi2hEbgDX9Dpg=,tag:Xnn85E99LibtB/LWwi5wcA==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:Fc0=,iv:UFqMFeZTJZtFMoNeusjAoSI60U9StZS4IXw+n8JQCRM=,tag:uUvY4+jcjiSJHs0HDS3/7A==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:NUw86IEw,iv:gh4Vqn6uteYnYIFcXT/QVxRPKm+G+j2aIVuV71nZ4k4=,tag:nZA/x75Kx4+CoSx66BsvkQ==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:qex150pm0eGwYiiI05y9LUs=,iv:5Tk001T796c4HFikoh1ZVB6ierBfDqOUkFs5ilkr+V8=,tag:LiDAiCH7w6aRS41VKIlIKg==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:nI91,iv:c1hBWGMkVb+vg+MKNzty8CFfXxQabXEtT+EpiuQZNvo=,tag:BsGtJDbBadDdTa687+Sdag==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:3rXawlSD,iv:FUtLK4KP7v/SBF08ZcU6siIO1qfjlJirf2x5PyzAjuA=,tag:mSIQ+Y43+wDfZGRbQ6a0Og==,type:str]
|
||||||
|
stringData:
|
||||||
|
AUTHENTIK_SECRET_KEY: ENC[AES256_GCM,data:i5tVOWnN9a/cfhs6gckveRxUpiN7Ie227R/74kOIz6lQ5US2hkf03W5qSGCrzheL3DB0i8+d0iJYsApjn/wCiN7ndZZVFko3W5jDfVPybxc=,iv:djF1G9JHT5B7ziTokA/UuPpG+tG2LvFU4uMAR0x4M6o=,tag:vHOd7jLYkM8Evnr0QUVrCw==,type:str]
|
||||||
|
AUTHENTIK_BOOTSTRAP_PASSWORD: ENC[AES256_GCM,data:PjvYtH03CzQbTXEqt/Z18+u1zJdok7DLwDn51FUZXDA=,iv:pSwSm8hB0/Q8uOAcOZ1rTqq7BF782EglG19GGeghm1E=,tag:qGCQFWCrwsWBOiQ07ySMUQ==,type:str]
|
||||||
|
AUTHENTIK_BOOTSTRAP_TOKEN: ENC[AES256_GCM,data:1tnekGo0GIemJCMRenjN0yNeSiiMl/kky8ms84TsqBLaQ0lQzPxjq9qtgNJwoAsufxrznrkDVKjeOEHNYcA82A==,iv:NNFWSLMILoWccA9ihy1OArSS1X3OySB7PIHdXZdyzlg=,tag:t2w+KyReOXftTrU9pBQDnQ==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSZGRKWm9PTkNSOENtREgx
|
||||||
|
M2V3Z0xxOXRhRGVRclo3SzBKRkc0VmtCY0NnClJPSGJRcUlzWmNFeTZ1bmorNEV4
|
||||||
|
U3l3TzVlMTlrWkJnQzBtUTJtcUwrM3MKLS0tIEJrMWZrbllYYUZKN1V4MVMzcXAv
|
||||||
|
eUtheHlNMHpsTEJXZ1FTMFVxbWloa0kKLRkE1Du+4gdOLerOl9y0mZw+8fqfECQY
|
||||||
|
uP64Q+9BSuPYNiTLZvYDzmdoy+KTS7i7C9rf+iU96R/crbQLamV9Ew==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T20:37:07Z"
|
||||||
|
mac: ENC[AES256_GCM,data:b3pDx340d1iZxsPPXzACckoLBrW+XjX1qlGrWEEI30j2Ao0hewTRZJyHbdI97A6b/hqzBN/4tjqqkdqm8goNhUQweMLTw86X6sALuE0aLS9wK53qjqih8fQwQBbgLDtjFXtW26h+smIm7aB5ZSDvU2le44HDXuWd83sWtCavNq0=,iv:ZMDsoKpygFJrYpXy2dA0IlxAi0jt24rxF72eUzgQs7k=,tag:WWE7G7+c9JK6fF1U9V8igg==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:qhU=,iv:/1G8RSCBaiQ6msQEPPOO4LzlbYOzEbNvT9OwnY0E1JQ=,tag:xO1Nw12oNk6wFCmw0YldEA==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:U/sQsZyV,iv:KMW7WL9ZjTmUz3NOQRhjsuAH2EZFXGk1P4hhl7cp8c0=,tag:0WiWyaaT1hmmVk837dFQfg==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:tQIaIJGaBzzLqa9vjPVlyhEymNupmKl6yyhx,iv:1qXckUq4RTBZ06YNLXsuMW1UWRK9cgymDdVnmwLZles=,tag:J2e9k7fmTYhhUFzRfkNYuA==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:bWQZWAI3UY04rdp2,iv:YCv7jIXpkzTx9iu9ScwHme8Nc8iqJr1xn8yXK/OsHk8=,tag:Xq9mX/EUHIOClV/YUm2suw==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:lQUZvLVW,iv:iseIGnyGeVE76BORrjX/TnvrfzaRMOFrQ7oWT0jIS9M=,tag:JMGEqkIwlrB25m5E1hBH7g==,type:str]
|
||||||
|
stringData:
|
||||||
|
api-token: ENC[AES256_GCM,data:i2P1Qm062lGubNILbbpzX/AWga7s+RhZ3ODsue4oWKZ7xMHvhYaLUG3/t7YEOmu2qxvvdjQ=,iv:uFvlesWdZUbiuh8WTq1uLBRtuptV2tXxGJ6PVgu7vCI=,tag:PXpdMfGR5Yui/e3PGRUWZQ==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3OE5SY1Q4OFdsSkJ2Q0NK
|
||||||
|
TmJmdk1RUEZHbS9qa2hPQm1hRzg5UTFKMGpNClRNRlZ6a0hjdDU2UkRHaDJKbmdW
|
||||||
|
MlZhTFBubm1HcS9MZTltQnNOLzBmNUEKLS0tIDZaSko0VGZOSm9EcFZ2L3V6cHVU
|
||||||
|
RFJ1bFY4OGJYb3pnMTFTZ1c1NjdoazgKhwqR2KeygYCpTR8u+pmMYzjNj3XyuhKZ
|
||||||
|
EBXYOrD3nR4JSoULAUHnZto1HwVVf6/SaR9rbg5OunOlCkUdopLNig==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:loYaADf5M4WMJCruMwcBHuF+MtQGYXrYv+MTNjtYRuk0yx4DGoTh2xiHMIZF7S52OCUoX4bn9qKqKR5sZK+XOWxD2hHovf/guo94HDklRqJ7ifDRshTz5wIlav3Zl5GgxUdxea3MmebghPEEm0uHVnvKxVbKBEw5WivCJd8JOyM=,iv:rYFOxaz/Lb0loPNVmXL06BPAl8NzYHn+3Zeoh1YjB+g=,tag:N+qbLLR0YhqJ5xbgTVvLdw==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:Rec=,iv:oFWV5gLi/v/mrA+i6n1hDONVS/iLHTZDDro2l3fYxZw=,tag:zYpAitxXQNc3QcnV0XiLXA==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:LpaVYuc0,iv:Vcmp/GhUhLK2/e39lpAXrzcD9CYstKjEojTThW+tyL8=,tag:yH47tiZkMe7Y7TKW7G+cKg==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:qHpkh1x88s//GHx3,iv:8aS0Q+HsUdsoMvxfYrz+hU+0ceml2mx44tlwYJOWUDI=,tag:SijqQ++2IN1tYa9M3GS2YA==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:DYQF9g==,iv:q4XjDTdYp0b6dBaSXHTK86RZVCXHWV8I4tsq3ms5z38=,tag:25hy4cqSumcJajA3DK/7tg==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:LG+x1Pzr,iv:ULVYrO9iIoZZCQDRhVF8WaPx31MBj0kXUJpKKgkcbyg=,tag:muo6E5YPTppgK0Vc2yq95w==,type:str]
|
||||||
|
stringData:
|
||||||
|
token: ENC[AES256_GCM,data:Hwwo8E9H27nq2A1FTB+Bx3nlQzowMkqIl6ChiXot8zt4l4UDjHXZPQ==,iv:kFiwyo6QZxwAPTXT9L5uXHbLhfULuyUv0eNSUe6eHhA=,tag:bSRKlb/J49Y1dumCphU4iA==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3bXptZU9CSXJKMlRJM3lZ
|
||||||
|
bEI4L01Gb0dpTGR5S1FTeXV6OXRwNTJmY0NzCkYwbjBSaXdjWm9iMUNsV0JscXc5
|
||||||
|
cXFYNkFLOFlXb0QwSkgvQjZrdS9PSVkKLS0tIE44Nko1cnVrVEN1bWpJajdFMFFG
|
||||||
|
V05lUHFiSzl1akdCUUVRdkQ2eHZEdmsKYXymkNu6pvIN0DW+3NYc95igGhzmm7MG
|
||||||
|
hH8IRVLLl1b2cW//wQsqngEnm8+UBtv09Bd7fyvfWbnoXqVAsshCcA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T23:18:57Z"
|
||||||
|
mac: ENC[AES256_GCM,data:lnokC1HjpIk94/666+PCbkkN57Rk6oDVtl6hbaM928honuyl25AU/dEbDYmz7VnaaigES95T487p0LjL0duePCoiZIigs1EKWwcPmAnfAo7aOZq8ZaOZb582KdjkAjJWVq+Ie9QMQE+AsDeKUAcE6QFdyQIKQyJK0pqL9YdXhxA=,iv:8Wd1340UGur9ABo6oC20yCSFXWwnFiN7I7fa3dg5HWA=,tag:W/pswWyRfvq0OcPh8rqSdQ==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#ENC[AES256_GCM,data:/JYNMOHrdMY2jobx67MjbJ7Eg8kHrNrkJcU535NRYLvY5nnSryk+asK8nQ==,iv:lM1jBHIxFkQriZ6BjGRXAlymUs5nX4Kvt1WNxUeZlkU=,tag:4pttiLNnIagcN4jTZ7BTwQ==,type:comment]
|
||||||
|
apiVersion: ENC[AES256_GCM,data:oRQ=,iv:0TzPcIoozs2MXJNXkzgcVtjjBUgfOHaSXQZiD37fb+Q=,tag:I1jwXteT9m2Pvdxq8zNtzg==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:zSxoKLqf,iv:Mf2s3h8++Vxqb4JoymHXY4/WAknDZ2GGrVVtKK51JxI=,tag:MrBrvl/FjIzS/AVoIa1rBQ==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:zKg/8UiwWslwTcesAQ==,iv:jxbj7Qtv+DRbhzTdvtv+eJuTQPNIf497NZPYA6ld4s0=,tag:HihBcoDLi8j9PB+uC265Sw==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:885ZKA==,iv:4PfWZu5qVGXP3ZzRHMrh5N9dzJ3SoUPPo58ppcDTnpk=,tag:gvBc+uYOmERckrb4KG2Hkw==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:eqT421de,iv:EZHnf1h1L29G1HOBYBSBeydNe4nC8XiBOw8YEL3kxrY=,tag:ypD8X9pa/9dbyChAMSIS6g==,type:str]
|
||||||
|
stringData:
|
||||||
|
username: ENC[AES256_GCM,data:Oc6vLw==,iv:1+tLTAxrDitXJwCAEccaVQzc9I9lNRgT3FsxO2NPDDc=,tag:r8lNDb42iewK8Soo9gbPwg==,type:str]
|
||||||
|
password: ENC[AES256_GCM,data:Ckc4PBc9tqigmcXl97iFUIqcjPzuJivT0tSmGunIDszFLU3u4UqAnA==,iv:jAwDFvJfQ1GkeU/qpEVUAQ6cWqxYE8nrgs+/RouyUxg=,tag:mP6AoUJP0T78+O9krL/eCA==,type:str]
|
||||||
|
email: ENC[AES256_GCM,data:qt+Sj2rs6l+L4x+kLUSaxlrN,iv:mfkMj3u8W2ZX4N4IH39mZXfEp+xphS4shaJcFOD/LEE=,tag:dD1jpIvTvPNOTer5gsfUqA==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0S3hzMFVxK3M4WVZzYWZF
|
||||||
|
a3krRjcwZlMrNDZ6d0FnZWZpdXNFc2tIYnk0CkVzZ0JBbGREUU0wL3dtSTBFTTNL
|
||||||
|
YTMya0duY1dlTWVadHAwUTB4cUJlSzQKLS0tIGFrWm9VRURmKzh1SXJPb05mSEdt
|
||||||
|
NmVUSVk5SnJGOTRhdHFKclhPMi9tSUUKCNhKWvUOExgirvRg3KaeEE+mRUPI3epI
|
||||||
|
xIYaTsiHvffmjc5mbo2sD6H2/L0h1IkLb78FJQdnTG8zl6yyFzKlUw==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:q46TmZXbALBGfexqqVtLn1YGp0GfWDzq6SqtEC0OujrCExHUyGG6Tb7BtkQDKCl68vkq0HkEwt9tKopGBJhUyC7gPNkXKZ+YmEEEdY84C3ePyvwWCAQ24R6o4Rpg8QURHSf9JA5RmoJOu77OcVMHkOB9Lan2K2dQVb+79WDx5L0=,iv:uQE/b1A1ZTgSCnk+wvwbHo7eWRW/IiKrqG1wlq1aA0I=,tag:WWXtlUi0jagSl/gKTIflqw==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:rGU=,iv:w1yJeQvN0MPZlPx8VQZmC6zkRaHW4rtjjROea0Kx9X4=,tag:qgYgVUGMQ2BtN0RZ0nZYjQ==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:wp2QP1he,iv:C8c/jKh5pnBOtHqXsTEchGf9DACU/5hOJ0Mp/+Jk1kg=,tag:QUZNeguRXdi3JO6Ov6GMDg==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:jVsnAcFTlRiKxDoU,iv:bWEqo28kH3bxzfI3ipiY6dsqhIcl9wIxBjFu0mxVGLg=,tag:PVWTveI8xhvLxvXPldhimQ==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:ZHnVZyd3wA==,iv:3evUnFTQfRz/ZilUQIl/9dsCba3nsX6o/iQsGXMBfgM=,tag:+9kp+VRu1ksroDTbTGmGPA==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:bHX+Bu9o,iv:8qqNddsuw4YoHAuJfUcPIqjcqNySIh5Ln8OhsfZxuk8=,tag:P7QGUXo9Io2Y19YqlWv5sQ==,type:str]
|
||||||
|
stringData:
|
||||||
|
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: ENC[AES256_GCM,data:qW2SQSsGD0YupU2SMzRaYwkLRjXAwV1OJBnhATpCJrOHWbEytw6IphOmPPd3VZKfHncekA4agXi4K3rUPHIx0Q==,iv:NCeW/UfIl0RvHV39ubN2mDfFp7GTQNuFv8KiEF+7KOE=,tag:lisx4h4PqUfLwBX8ca9Siw==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrUkxLMEV3aEpILytic0Ux
|
||||||
|
V2VEV2EvcENJVnNNRUtBbGkwT0d6eUxYeEFvCjZPSkU2MG5qME5jZ01mRVJwTUFL
|
||||||
|
M3M0V3c3QXBoRml1Vm9NWW5JTlNRSG8KLS0tIEJnNlFmdFJXb1BPaVJzTmo1RDQv
|
||||||
|
VXZOVHRHUEZJRUZ4eGZGM1RxSURyWXcK3ZyYCPhRUvpvT/pjPQGJoLIwaktZY6Tg
|
||||||
|
LS0cIDUhjPFbeu7qUATOrat6vMUi0UREbXSFZ1KyAjNAwJZaUusPMA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:yUBBV7dLEPld9yJacn+YaOT8M1NJ2ploGIxV66aVOH5x4tDAsskcUQJlzApWCQmORuCuMKg6qfpGgLTojfD5msha+ufVVJXHl7ua+STar1/FPJXaHm+z7YyZdsD5j6f3AH8q18DuLy2DYLjXELmPBsp50g7MDJEm3xLhCUXygjQ=,iv:xl0OtA/2umz0YH5b+6QjlAByZWbvfGOmHqomziNjlkw=,tag:xwkQDZx19Lf6QSwU2zPYTQ==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:x1g=,iv:ImbG3BggYpU11BV84Kwt1NrwoSZIrsoiZovqPM2ziWQ=,tag:V5clyseKFF/jEUX/Z/6Vmw==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:9NYfGQuq,iv:6ICJnhTiMP0QFX2xQxqE59AkbtFXQ3UJwH/v/2KaQdw=,tag:wHeJCUeTz4CkqDE/3sDpJA==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:vvZxPCX3S+jdtrkSZA==,iv:Aw9oxB0x3eLODe/XVGKd5A4UdKoW1ChbRM7Un0kkwFQ=,tag:1EdRix+CoNztbtXILUlHJw==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:ZVI3KHMJ9w==,iv:mf2ySxEaRrsPLgkwUUjmJuzklo74t8UTeCsUean2nx0=,tag:tOloglgAJIDNT1xNr//L9A==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:A3/oiYOS,iv:OMjn8JxX8g/jJSsIL911FX4autWEQDk1UGzfnnfKSjQ=,tag:p/8Rc6o7gfxoX5Lv8lhWvA==,type:str]
|
||||||
|
stringData:
|
||||||
|
admin-user: ENC[AES256_GCM,data:rjpH7gQ=,iv:UwIdUTnI2soFYGjZz6xj8boacSMYHxTg0Bl1xFLllsU=,tag:h2nX82mM2iT3tZu9SV9R5g==,type:str]
|
||||||
|
admin-password: ENC[AES256_GCM,data:hN1bHBVi9SfVzZ1uWA==,iv:4V3wlpMMfakInQoqD6gxk+0A6lWCm3aPw31gpa0sKss=,tag:na4lAI7SzbWTUB0lQ4uATg==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3eVI0b0pIZC83Nm53dEwz
|
||||||
|
TlV0alJCL2duOEFkSlV0bXp4Vmp5V0pwWVZFClNGSDNFeVluV2R2NUpEcEdYc0t5
|
||||||
|
R0xsalVTd2F5c1EvOXJPdHJ3Rjc0N1kKLS0tIGg4c1RBVlpwdEFQd1l3Z0RQYTBY
|
||||||
|
N2t3MVRTdFlPS3BvY1FJQmZlVG5ZVmsKEZx9fdx+p9UpzLGhN8D58KlPAOtyLtVs
|
||||||
|
ldAPmMThTIWQ5GgJjLwmag2NlMyO8NmQY+dd4a//Grx/4bep9lhjTg==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:YHngoakearZDcLH7ACS6Nw2hcf19XvVkthLxoM7KrWIAsa+Izkr/kr3PGgfeuwmQL0lux2P+GPlvLE4VeovAyPOiEBVl2tQK9/pXameqynswRn5wnl/kUMPJblNBtPpKR3jHlKsSD/o1MtQofm2mFLsa0bJ+C9JomAIYMFMKpms=,iv:M20AveeOFJs9Jj0jECODihC/nxIe/rPBU5QjfSQvuvs=,tag:dIDq+yaz6u87a1BLQCw0eA==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:uiQ=,iv:vknDc16Zrz/lxS1WG3ksG4SHjhJSTDdklVxxwL8eylA=,tag:1srpVd6QTYQGGGYGXT8Cbw==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:K6EPYaNE,iv:VWzawfTDNlag+shjBqwSzZGcQQq/TGxeHDqd4fAh5Vc=,tag:ncPqMYjq/73oMzphWhuuFA==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:vaRNQHFYfdUz1hzuYO0JxQ==,iv:W7aUJID5mxoO1NQAcyPDYV1FGtBT8myKVMA3s2WgrPk=,tag:4Ef4GBtTR3dVanscvigRmw==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:HDlmfGwad37/,iv:JFPXxz9xp5vDJRcWy73SYGWDMyHKWQq9Bvy/NJTwTnU=,tag:2ldVXgzKn4xp1rZI/VYgUw==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:UgolDLIV,iv:O4himI90UVwx+OjKJXobu10Zg3a5IoQcjAORsdhaWpg=,tag:xltntRsamhQkPEoBNtMytw==,type:str]
|
||||||
|
stringData:
|
||||||
|
oidc-client-id: ENC[AES256_GCM,data:RLdxXY/a,iv:US/fBReK7u8WA4IS6TSzYiHQkte2CuVgugIMpA/pIlA=,tag:QKrHGQrzI9YpfNQD+ts68A==,type:str]
|
||||||
|
oidc-client-secret: ENC[AES256_GCM,data:SS+I92xEkOcJHTfWvZ1AA8hh1bFAtND5adZ6Uh73nIPiqAo958GSag==,iv:9HcmXWrGtflc6WWQe0n/pgB1Xveakofc3hvz5vuE29A=,tag:0emgeyM4GBftVKn/fx7XPg==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCWkR5V0ZYSTBSSU1OTWF1
|
||||||
|
TzFxdnZkUFFIcGJWZ1VUU21ZUnU2cUdob1drCkhVTm02dkZPZ2lNWktOcWZ2c0Vi
|
||||||
|
dG1PSmZBR2M2anA2Z09iYUU0SjVzTlEKLS0tIGF1eVBBUHdhVk9YSzJlQXVwd3RE
|
||||||
|
bUd3UTlwR1NiYjNlcGpweTBhdmk5b0UK8pwUoVCr1uBX9dQ7EGRgFMAPk/z8N9Fg
|
||||||
|
6GRUORFug5h7x5kjXK9sU0yTHjFSekwBi75GLng0IrLEGJxHVM5wVQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T21:07:30Z"
|
||||||
|
mac: ENC[AES256_GCM,data:NupjQWBJHMOo3/VX5n5nRx+RFT14N71XkrjXRC/VCP7i7H8yn3tcRdH9g14usgWunmqg2KKpGMUIZ9bsQ9Vds9sBvUKvLyRkfQnPwPh9+PFCF3ZZLPgzqcT19kg4I3QabmO+LPzQXsZWLKIqgF9/B60EVIgYyKhZmP7XblY5AVo=,iv:r0deq8+txE8TwzdtxOMaIAZbZc2fu+Tp4Jd2HZGbkhM=,tag:qMpZp6LcWjJHYMDp9ecBkA==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:FO4=,iv:kKIuJp4uGeX6vECsXk7w43F9S281jObQchmK8r1+kmA=,tag:x5LYd34dzNDLCma/78M4+w==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:o4RTN8SK,iv:uyOJWjDmHIPuBl9Wc1S/HxUabV2yytWr1bntHW68sTI=,tag:3On0SnQVJ/ICMMryQe+8JQ==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:a89dHmLdSeMHE7ZP0Q==,iv:OWJvJhFqJ7A0UGVJnZILzCJ/4VuVFib9sUDjXuwyqJQ=,tag:1XrIQn1GDh8RQ4vmQAu7+g==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:4wBVpZJgQGM9,iv:3Tn1u/2TP/+SnELu34PDf4Jauf2L4f8+MtgrPIlyKzk=,tag:L3qbKFBjm16g8KX5vUsK6A==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:mdrF9vUb,iv:7ukOgwXlnRf5mpJ/+6YXtyGRonnqehEgT4pJJIafw7c=,tag:90mt7jpNb3N80kCEi+R/rA==,type:str]
|
||||||
|
stringData:
|
||||||
|
db-encryption-key: ENC[AES256_GCM,data:NWJryAEd/eUJxbNiu05RzLF/l7GDxIQM7RQe3xKBz6XklgRAFwQxxjakcVd85pePUBaQYz3zttFglWhfJlWhkA==,iv:eUWbcs0B/mIheyAQ2+DyKgcA98CRKQGAVC3O0WIkfU8=,tag:be06X6AQqlohYlxuYoZiTg==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1Ym5yQ1pmWHZjVnYxbUVO
|
||||||
|
SkZ5dnI2OXR3cmZjZkM0K1ErMElLNktlK1JFCmpleFBrcXkwcDA4cGFuWWN6VDQv
|
||||||
|
dVV4NjB4MjEreENVQnZ2ejVLblRYOVUKLS0tIG9VbXJvUmQ3SzMzN3IrRDlac2sr
|
||||||
|
eGZNYXBoZFZDeisrVCtUMXVPaEpoNFkKjwhUU+Zsqv4Be+m558fET5UhF/2rO73J
|
||||||
|
AhGbJVaA7PaOCswPWiOLso26zD+MBB3VKHNdUFnenpoM16Nu4pOf5w==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T21:07:30Z"
|
||||||
|
mac: ENC[AES256_GCM,data:KrGQY/eOmihYCvQCbfcM6bZ6rfx4zKh62M8NM0VQr1//1dSHpOOh25dYGLuQeECnqLxSpGQNWzr2xjUYWs4NOdSEFGq6FL2iHE3LnqoBZspp/JFJYP6+z/8jSUojwKEW7OuZZdPN6gF2u+IPOeWbGftnjS7X+atoSkYag2ty2vk=,iv:IOPEgBizsxrVoK5oSdWmi0uCBUnReOEnMYXuwSBKTzA=,tag:9k/XQ/AE7wSFq5XMbY0FTA==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:4OY=,iv:0Lgui0+X2oTlgCX5HEIIihEsGJD0C2+pDIOTCNi4r+g=,tag:uL7vlreaHjcLJjV7e5I7PQ==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:x/ltkind,iv:KRBfS7COsTs/l3Fhyk5FvH7IVc+r/6M6FVOo+kPDe1Q=,tag:UuTbe213hDfK/WUtKS/doQ==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:LpOEecTJBuGy5PqNSFM=,iv:Q+DCLGtw3wihhdAgEbzYHyQhFTPWFyE2iJKl9mYaA2E=,tag:E2wrig4lRXGDvSCHec5clQ==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:6kMVCXaF68NO,iv:gnCCkV7GKXA2HWOOeNUGnpAycf8U7pl6F8Vfo6DhLR4=,tag:0VdeySsuhUr2heXyGXZwBg==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:6muC3Zqh,iv:gBaC+6MXvZY/bIpy/cEG/zWwLBaUFMVfxAHa1OnwfnQ=,tag:v6NYyhAKAwovluHAXNaOVA==,type:str]
|
||||||
|
stringData:
|
||||||
|
SECRET_ENCRYPTION_KEY: ENC[AES256_GCM,data:jX9ULlaBYTSTFGRMplSder5rMrsz7THU3kgJSGef0r5rZvtoaAG9NVYU2QUEuD0sZgfqRooeMOkDCAqFc8qsuw==,iv:uOmx8w3XPKnoxfasf7gqi/bljxigpLwaAC30c2TroKg=,tag:Dmbu6m3+UZa1w0UR0LRh9g==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6TDVyNGRtYWgzMDYwdjdr
|
||||||
|
NXJWcGJZNUx6YzBGWm1nWlp0SlN3WXhaR0VBCjZ3bDBmUTFCUjg3U21wak90eHZK
|
||||||
|
NlRCTUZQSHhoS0ZSUGZ4UHhST01KNmMKLS0tIGUxcGo0NmJ2bmJ6eGJ3QWxzYVM5
|
||||||
|
c2Z6UkFjQmZ4ejIvK1JQQkZ0VFFIZ3cKA8UzmGsX+s3yOermOHqbnnEtekHCTqmC
|
||||||
|
R3Jyzf6OolZMJpNwbetcsOfgO61aM7LHl/Xb/hFLbLjZ+eECrt8VpA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:HTrEEVDm8JqnVBFaY/FJknxma0nI95tRiwVc0K76k+2R4jgKCBA3EUAzPVS+gA4rTAeKHPSHrT2eidlkseT2WQUjqMRrpHcUsbSjNfpTEY2g/U0PIiOXZMRGfwmsQVgmu2fqeMOyzDSyya0scdWJXPraRPNZ+OpEkNVlm4rWcgs=,iv:KY3dnnmooJgsu66wc7gbzeIGUXZNgVrxAhpAwxoG+G8=,tag:7r6d41oTD136N57rtY7auA==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:GjY=,iv:OKYWMFG0fqNwzg1F6Jjoon/DReOXEE6RPM5g0Dn6bF8=,tag:qLjLi9LCOuWRjagVXpMvaw==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:0gG77TDH,iv:9WJrkz0BDDllNZskrkqiQ2p1KYWU+EmKL+vetxIIGKE=,tag:UhU8fbO357HtxHAsVFeeBA==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:Lwk4A9vqN3cnrI8MrQepxf1w,iv:AZUeVqCpdrYJM/chE9bzmFOK5ceqPjqsqMDcQwliyr4=,tag:vbY81yhptOmD19UITJqFmw==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:UTS0X0QkMssEiH1l,iv:6ubXogCM03/cTdsUP+JWOSepjQc+ZamObyxINKspHeU=,tag:jOj2q0ZgKxWMxkrgfOct2Q==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:ADb2WnTAeWRVIHdzvP/Mumc=,iv:h1CCbwf3HqYWOyTX78GMfvnn+PGfrKWuxahe7/j9aCQ=,tag:bt6xtaqHPZ+E1wEGKgKDww==,type:str]
|
||||||
|
stringData:
|
||||||
|
tls.crt: ENC[AES256_GCM,data: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,iv:AdR04DL4zz35+2ajdPURF/Hya7YJh730XaPr0FIV9k4=,tag:SEnPSA1SrQIhoSM9mUDKdQ==,type:str]
|
||||||
|
tls.key: ENC[AES256_GCM,data:QW1SpM0wHkjtO31eHWi1CpXGCqngnuMvNgumDji9pQmXGl2rWD/Lpnc+/Dbzs+FcKjS7gEq1EMFZdNuyXOFeaHEWEA6bUfKPcET2sro4b68qyks80ggi0nclLp8t23I2IZ8EORAbhIxS2LNmgYa7CGQjZ+lIFIeMcLg8T304TyUlO05PZJJsd3+sIm/WU4EcshqQxpQVvDpZDI9VOpQBrm9nimECKJUP8f9DqV1QxCwLZKbJfd23qcNKDvvkJPDFtoYWtpygs4RX+NghNao6o6aK4b7ci642HVJCLC1nYBIZLHWaFpOdXoYq/VdYRZoY7DfNrwPhaVxJprc0h5qkAvVKvK/nvGuc5idZcCAKrxPTqconqbCcyfXfRKJJb71Ta6+cDAhN/Pt0MGbDWW8z9+1KmlFfP6PBW0NaPB5FZ/v42IQT4RtR276vJLBP0JNLM1qqBdsU6TFCdyp15zLy9fIt2F3phdD+gnwuYlQKTivraA/7IOj731pLLwytMzz61AxsBDKLoaIkaSJ8C10kWDRT5K29b+m2fLDfjJ1pBqL1Wn2baxGF5YC6GKeDwvvjuiMOPyzTIRLtQhwodcUFRz5gP/4qLn3T3umkcfwxXg76Ef/f+KKmcp6s4eproE8vnO2OgLEmjBQ+CmLhxeEMftE07wUhu3q9TPlOYLontzfc3SwTMFji7ZoJNFEAzFKcmYSmtT2bkAijXBYNhxVTz3ks5GtbEs9tw1/IYT8gsSI6N52F6yUBIydPAmAQchQO+ATO/o8+hFj81PcIJvdeGV+0/AdpLiPlb9qKmDzu1daBE0lk5oEZH0qIZ8BvDfBL4bGzryBmwnrFXEK2I9yRYs1wk6x+l6k0hEWJHs5Vg/BAMRecdp6eimbrOyvdWlkLLa1/KVPDfE6jp0c2Y3S8VBVKyWf3fBQn+UOZFMCLdZglkJZNBHC5JKsTavsTLfvp683033aHX+f1wefGiLkjzmZPUzZGRe80QRWk1Tf/+TzsCwkCwems6NTjgkjTBe5AQP0sTtFeu+56/NCGNsqrJyoljPDuvx32MUoBW7VexNatk059bz+C1i5eYqrzuXN4uEikR6IHugVJnxFI4XD/JNDhwyVzBmwn9Yf6zQwNVMh4vtFXcYd8osTgLI8lTU3RlbqixEDQZ3c1flki6ncubMQ2uYlCCaniahECL3U4lIFiILZYCaY8zqEUlLBB9KgGVq3K0WOn1gI9uC+nB6/OrvL2q9sU5r6Opetkp/J/nIPtRt8af3dHhCVJpH2VD05lxPn7rL4KQ5SLfwrbsFfyiXjAWMgiTR8PaXkcnvb1/g7YAbsEHkUiAPG2rcr+QbYD8TYnGkROh64hpTHrFjFg5Cfj0BdbNkUg2wuAWt9cJ7DuZeQctQPLj/7utzs0y+fd26vTo8fOd3mdf1fBxewauAIx1GuFgRB+4TbmyWcwb5mgE5tuiqstr4FgjhLn6Bu7+DMoaDyU3ZcsM/XzcV0GPYLWmw3xzM6oahjzA0Gpu1l9fVYotbKAhewEvq8o198JXO++z4ixsNJkRqS2Xo8f0rE9Qm4SCeYSXdBsrefFPcATd0kdHmAOl+fIK+y4crVJ0XgiJtXGjjoRwK/IrEO7LxAnVZUVaDOWVvSgcaWPZWeFcWjC7n9i9S5x771DJcW1/Cj+LGeJ5qNuSnwbqikJ2pbrR5VY6tXTgzWFLYCeYUrUWBb8Tbs2mrvqdP+Qj4llT09XjcWe2FiWf85I1hyX47c0vckLeI/8Ly46qMWIctukEQvT+ZxxsJJ2bvMTcJbSTxxf/doCqB/VuMLnPQYWroPpJizw4Y3vZY3x8se1r/cVA76UJ9p7FIeilB+SiKyRq9t0XFtFRaAQr4bvr4EEWRRy4c5o3Z5S+ik23WpSCz2E4Wagkr8w/B0MtAkCEIdWcNGLYq+bnr11PCVIjDwoCzue2HzYlbHaYdK5BR4PR82/7Ls+JXreXPnkXkPfis+H9651R22X9YodqyvvtpnzXTLmnluQzpueNPl3wUyj3tge2NguNz3zWjFIvH7zefY+Ucc7YLP7565gqKsNlzeg6fWPO+sR5kGny8WxvavJ4bo2ILYSkvDKRkiPG5ZmtbhWK3yiUJfkcr9ggWtXDB31wG5L5MKQEqqhuevPWmbyso1N9VGHesdhip0HK6hzfmCx7/eZ78y0oGFy1AW8Gi66mm7lUzyRIp2GjfWq2VMVtKlbcdjWIjRAGzJK86HQR4FPvK7zMJS7uQdpwGM8ulXrYYokraJkvujPr17JitcTO9roMeFYUlsTyzdBLEk4M2Wsc+WU/rg2nhRmoNGsGwNzh9SKJLYl6IgRRQi4hDPKxHE0EJcZNs5VRWqvf6Wfp+cewXnKoSQWcBAsNa8YUXn3G/7FOhEszm3fOLBqoHJXhIzRzVTG+1B59QCMw8PRtkCGDgaOx8OedLgLagh2YVJGWvkDteOV95joIQU0RC/THZSfY/qx27Z5A/RBqXRTqXq6+A8Z1DtNIL0z5ZWC03oSIMuuvBrxF0r8p2NhmISvgMFQf5UT9XlNZac+U5u04+SwrwGhLwSqghL4qgDGJK2UZ/s2lKRfv16azIDc7faTBwxUCC4xyRTe+Bv6PwC32XjN8L7RPvupvkjznex9cvtIA0VMFhgDgrzpN3RLLT9eMkgeTm9aM2KdZR9V7tnHusbVkS6P9U1ZaQH4K+QIcIk7DPYwYYT+3HaYvjgqFijzzhM4eCGcCET6uKootP6mW+e/1hPrWiKoAy16i0dF3v7sBmj8FifU4OGMK23jHs8kXfMBmN+ltZG+RiGb8AA4Y0zh6KQoG3+wGHZNDZWMudcr5hMNjS+b6ZaIFpYNFQnmhpdQ5OdRWGS+/BuCTm11GCtoEEmqLv/eDAMvZxhZxUe7ql8kOfM3cJivjidxguTpFfMN7+9LuXFIMlrsoTTE4ZSHHHvpoTdW5KiFvs/tjHN+MRAGUi+AfHa11JA20XK/7ce229pZMxFe24ugILv3Uqlq5GZW+RLMoSb1EfwDF8eZ8/UybcK5TUG8NS5+WEP/PDCYK4MRP+hmlS6zvEo7mpsk1Er0Z4QnyUyRRkZe8XrRCA7G5hv7I72mU9VCoVpzx4kKLiyHcIjUfWunIGodP+dXhEqulVMRpzaj/iY5CXDfOiwpXX+/4uXcXf2370OhVJWGHe/+3N+1UGXgdBDBx+hFyS+mTWZbOpZROsrHfP5bHYNXFtqvXlL4oBKttgUsCmQ6ujcIk0Gi7hKbvad76jFMvmi+cc0zBwwylGf7QWzFa0AtwZTqNgnHYrkuNqOTXOfmdMfwMVv4ObBTpG26yGg6sALYI6G50nmJesA9qMn2l2A3mQGXskbppvmtLGQSvamj+DrNbUSGbr5Q9Kn4bptHfsoidQIHt5pGBlxU5msivz5RL9wUJtEdiU9mZbGJ8KrQxgOZaOQdFHwh/k8i5667n+dUUjb3tzQ50B5NPIH8etNdUOy/nslNlaiu2daYTPYnm6GvNo9CLqUVPi0gNBs4zowW7yuMzG5xLQUcYD/mg8V7HHbnMZ0yzhIPzVa2cOPucDK5wz1SSF0p17xzuZkEzIpZ256pXif4zI4lZAunayiGoIq/elyqbhnbMyGdvmY18Xs2p7zxowebXzyJeVvO973jDNYj8aGpoiQV4k6Zg8ZU23F1MCSlobp9Dp887W6y4MZDjmkvQi5Bn4qfYGJukE5R0grd2e++bU1x1RpQmCAg6CKk8ONFYfTJCBlKnPfXf32mS6KQ/fOwzFr7RkBGjTGESeJBUtdGVfizALoFMFEOWuaxRUSRzWazOMBLHDTwK140S1XNZgNO1oLaBfb4Q/Ef3tpo5VcRGGkY1j7XhQK8UV0R+OGJJecnLvrJWAeYfg3wp/vOO/f18oVY5uR1kyduqkcRB7xygJ5lyUu9uUGcx17UzEGJt7oRRiuz+ys19ZYGZ9P02ca+3xBSd+g8ThmdwpRhbb8xqEAhktM81YPRiSPKbXCR20Wn3fg7I2Hdsh5ISIbhFD5+FKuoOPwFMmcWz2yVvsVyiTLqIzNCsJgxl9rxI4GEPbaAq3QFfMq+nMqwyrofGogjiimAAYYk3WEXFcRdXMXw7TT7eXGWfdLk2hbU9M5hvhem9+hJRZzQqt1mzoJuJf8fqdWNq38Kp6GHPsy8WBRdw6ZPlgBvkgydfJSXAmzTcJUEAn69iQmihcnD90HNP6+A9u4lLzuhV/c5+yF0gAP7DH+oVW+46wUJttWzqRbMtz3geV5H4xKkK8dECgaEJcSL6Jn8GrE7l8XrO7qSPzaE8Sfpk4NVu7aoAEw2rXErFqAWFBjBnw==,iv:tM8kJdNlKHgDmFQ85KAvt8nF9aPo9FeyQ8SoeYwQSd0=,tag:OKhz5KYHi2tCts+9MjY5KQ==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMZitSSzhtNitMM0h0RUgw
|
||||||
|
NmVGT2RhVm1jcUV6WGxPbUJmMytpOUR3c1hJCmlncC9PZkRMMGZrSWViL2dVZ2V4
|
||||||
|
emNYekZHbFJ3WWVUcjhZK1hPSTQ0UDgKLS0tIE1yeEIyZHBaVDA1MzNDMnBXcDdL
|
||||||
|
VjJTY0NoaFZhT0V5RU5hTlc1b1BPY1EKSiudyVo2Tw3PTPGt59vRL8pkJlw4zFbx
|
||||||
|
OTkdkmlcjwXPahnQacMQQOn0ndo/w4yHinu3RA+/yAB2yR1BPn8uQw==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T20:55:07Z"
|
||||||
|
mac: ENC[AES256_GCM,data:DS2iyn36jrJgpWdocrloEFyXcSEPc6vXVj8XJwvEDyBAkqulKDeVQy4Lm60pMWqAt83Q99WWvTq2xd6eRrtAWGO8xmeFrKWaldUqFdXs5l7uscSAm8gQU7Mxp0/bx7rhJECoa/MJp+kv1SeDuon1b838vaY1eekMn/nJTSFbcBE=,iv:5mEWwF7ov63rA1JvFpizLhZne30hEuTcFlHu1lyAsUg=,tag:R+ig+kp2MtZMF/rXabCyBQ==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
# All homelab SOPS-encrypted Secrets, decrypted in-line via the ksops generator.
|
||||||
|
# Each *.enc.yaml carries its own metadata.namespace, so no namespace transformer
|
||||||
|
# here (that would rewrite every Secret into one namespace). Renders exactly the
|
||||||
|
# Secret objects — replaces the old argocd-cmp-cm SOPS plugin.
|
||||||
|
generators:
|
||||||
|
- secret-generator.yaml
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:44k=,iv:X+aWGTBFUqzlKT6iCR/LN5ldJcT0mSt9q3bMYbm/094=,tag:WDq6/nb4lzmyJ54QmrkDEQ==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:Rzk5Z+Hy,iv:WGtDcuwwxveu72V1Ri+rh44IQL4CixKsISW6R6Ai1AA=,tag:fTqJaqQpA4yDHd8hY628rQ==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:XOUsgfr64/dKRSMt6g==,iv:78DvUfW9uxmVSr4LK5HS/GlTchRsaTBEMmqjtHwAwcg=,tag:EvmBXKJSX3iQyjojAX7T0w==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:xWSxbuMa1Q==,iv:FDj3unj17suAuxzow1iLUztImC0dT5e5QuzY6uPQXV4=,tag:kp9Qn4/uWPgIFfoEchsOgQ==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:luksVJs2,iv:gYL9JD63cgNp3mKqPc9liLdO9n/+qOK+Eb92uV5Ex7c=,tag:ECPwT8UUnLgr3OcMvhdg8A==,type:str]
|
||||||
|
stringData:
|
||||||
|
access_key_id: ENC[AES256_GCM,data:ZPX1IGcFZfxquA==,iv:9jKQl0nZYc5qfO9mdvfHhfG1a/0+mZ+tmZm20LQ2gg0=,tag:Rlq90E6Wq7el0MEiQDmIFA==,type:str]
|
||||||
|
secret_access_key: ENC[AES256_GCM,data:49BtPfJS57tifvyoO5DhK9os+O52b7igVSjdB1h86trEsXPle7KaUCkFXus=,iv:Uvy3pUpYlsZQkxMG2tKnXYD7Us4RcP1DmGWlNlUp0Ek=,tag:SrVny6/3Gt8XO4oI/VTmGQ==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2MFh4Y2xTelhGNXV4SG5x
|
||||||
|
TTROWS9LOXViRmRnM0FDajBYTUlnZ1R2M0RjCkp1Wk1LY1E0YXV4cWpvZDZ3S1Yz
|
||||||
|
SmNaaGxZVjdBQW55WDZqckh0aDkrNVkKLS0tIDVkZzdQeERybm0vZUQvZlhCK2hI
|
||||||
|
T2ZlWVJaMVBxanFIYStZRi82dGZxL1kKfQQoQlY3vVbU3Ys7TiEbDzCv5zFuki2m
|
||||||
|
LSJ3zaaRAm5ldnfvX6N7fiTJed7Qo/rbmMoDSCrxj5P0SA4UhCefEA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T20:43:24Z"
|
||||||
|
mac: ENC[AES256_GCM,data:qxnq1Rc2DYN8VI1V9Qbsg28dZALu1dozbkcWpGA5T8qAHoH8ORo16ZgS3RclAmKys0n05DzvNtnVh2jXWupg/OW8K4Kn7Mrcb6AmEi5XZAlkCy6tzZEKdm22h4A07wy12701nIf5Rn2dKt9PO60oDZeR/5S9oov47sDXjy578WY=,iv:fyqI/RVnvb2MsGrxNO0GU0+HYgLpAm3PXXOnDGkAaNQ=,tag:dcguaqaj+trqgBfiGdhZTw==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:Mdg=,iv:u/JjNJfdyz7ehk8lHZRh1u9zkNuufHosyXv10Gm+T6E=,tag:pkgZVJid6kpGuy61+oEMlQ==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:/kWhhqyz,iv:DymezGpBHmKT3BM/CcUKEGIWZw/JXNutReSKfhfmoD4=,tag:DnA+316Z+0eqBWN8YLyv1A==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:xs1k//nWy1iJ52Q=,iv:NDGvM319FCypxFLWwONcF12osaU/EL0IwD0c2xtqYnU=,tag:6IhfISF08WQbxrUmT49d7g==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:jpUzI7BmZA==,iv:PQrfm67la6RJCRq0H7qpiY8n2hPUjcaK6gmJ/zPQW30=,tag:bp/3GYbRpYOIub5nT0zz/A==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:fndXB4Y1,iv:EXOe0day1ScxvZ7ozn1QyHgaZgnvlPNmvSQG74aw7lM=,tag:quPmKvFTjRqtZoVUEoQAHQ==,type:str]
|
||||||
|
stringData:
|
||||||
|
config.env: ENC[AES256_GCM,data:3RIQ1i6NtY1Tv2eJY8JXSgfBEqi9px/9TJVNrPf9+brtGy9Mx3SwjKVnjGXUcA2cYcNZb1JR7temXmznXQdsRfZ9yohe4oc1vL3yPGPY9YfP7AHGYj9E7tghu/rgkHOf93RmfdKv0Rrq6miBcA==,iv:0RQVMi2mMHBRTJ1OX6IDFvq9Qr7zpqgQzt+MPPgBIHA=,tag:HNXLKf5X4P3E+hc7YbDtNQ==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtUVJFQ1ErNmkzd1N6b29t
|
||||||
|
L0VWR2tlcEIxeG1CR1hXRkw2cEt0Q2crMWlVClg2UlNhUzdJZGpLZjJtYlAvbk16
|
||||||
|
eHk0ZHZiNzVRTWhyQzlieTU5Y3QraGsKLS0tIHA5UVhvUXhNSDk0QlV0R2RzR1dP
|
||||||
|
azVEQzgvSFNwak9TcW1USFNOb3RvSkUKNLuW3h3cHDSSCZGe9Vyiv5m+wXihtCYX
|
||||||
|
oJtbRK+kJF8oOUo4emI+SLGrCsWq5cG2wN/wgP+ChR1zUgRgfq5oOA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:hQfimKuhfiIiZQAyNDZQR25HZ5biU+kaWig3ElVPiqMY3B0LrmKyOEs7ittd3j3meUkPEFSmm1yYSXcdCuf6w7cwSbEiL5DG8t7QKPbnx/aRdyHAJW+1VC0e3Lye43gaHya0iUaWS/YFQcrT01oEbckLIDWPKfvA6pg2bZ4kMZ4=,iv:AydyWMVcjTQu+7Fzw5vtqkvTTeQEMt8y6snESoLUDE8=,tag:SoKklNqa9nH9G6d9+PnolQ==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
|
---
|
||||||
|
apiVersion: ENC[AES256_GCM,data:Mdg=,iv:u/JjNJfdyz7ehk8lHZRh1u9zkNuufHosyXv10Gm+T6E=,tag:pkgZVJid6kpGuy61+oEMlQ==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:/kWhhqyz,iv:DymezGpBHmKT3BM/CcUKEGIWZw/JXNutReSKfhfmoD4=,tag:DnA+316Z+0eqBWN8YLyv1A==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:XEQHsQqTgwukYw==,iv:wTw2eoooDA9EfB2df3jzb0IWUdtLSwcuKzx8yDBaihQ=,tag:oncXt2gM1k6Qppafpu/D1w==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:jpUzI7BmZA==,iv:PQrfm67la6RJCRq0H7qpiY8n2hPUjcaK6gmJ/zPQW30=,tag:bp/3GYbRpYOIub5nT0zz/A==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:fndXB4Y1,iv:EXOe0day1ScxvZ7ozn1QyHgaZgnvlPNmvSQG74aw7lM=,tag:quPmKvFTjRqtZoVUEoQAHQ==,type:str]
|
||||||
|
stringData:
|
||||||
|
MINIO_IDENTITY_OPENID_CLIENT_SECRET: ENC[AES256_GCM,data:IYBfNW19cTo8WkCGT4zuCljzi2bkbYWJ8DY/yedR2MJNqEO+bMVvjx0OqSqbPp6rnwGyMQOhBN0gXPbEVRDBGg==,iv:I1aGIikbHTKvh//4Inhp7UoNfhjlIXSpcyP4RqljvqM=,tag:Zh3Vh5DQYAsTtiW59D0/QA==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtUVJFQ1ErNmkzd1N6b29t
|
||||||
|
L0VWR2tlcEIxeG1CR1hXRkw2cEt0Q2crMWlVClg2UlNhUzdJZGpLZjJtYlAvbk16
|
||||||
|
eHk0ZHZiNzVRTWhyQzlieTU5Y3QraGsKLS0tIHA5UVhvUXhNSDk0QlV0R2RzR1dP
|
||||||
|
azVEQzgvSFNwak9TcW1USFNOb3RvSkUKNLuW3h3cHDSSCZGe9Vyiv5m+wXihtCYX
|
||||||
|
oJtbRK+kJF8oOUo4emI+SLGrCsWq5cG2wN/wgP+ChR1zUgRgfq5oOA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:hQfimKuhfiIiZQAyNDZQR25HZ5biU+kaWig3ElVPiqMY3B0LrmKyOEs7ittd3j3meUkPEFSmm1yYSXcdCuf6w7cwSbEiL5DG8t7QKPbnx/aRdyHAJW+1VC0e3Lye43gaHya0iUaWS/YFQcrT01oEbckLIDWPKfvA6pg2bZ4kMZ4=,iv:AydyWMVcjTQu+7Fzw5vtqkvTTeQEMt8y6snESoLUDE8=,tag:SoKklNqa9nH9G6d9+PnolQ==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
|
---
|
||||||
|
apiVersion: ENC[AES256_GCM,data:Mdg=,iv:u/JjNJfdyz7ehk8lHZRh1u9zkNuufHosyXv10Gm+T6E=,tag:pkgZVJid6kpGuy61+oEMlQ==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:/kWhhqyz,iv:DymezGpBHmKT3BM/CcUKEGIWZw/JXNutReSKfhfmoD4=,tag:DnA+316Z+0eqBWN8YLyv1A==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:rYX7nN6SYoG5HycaNNrprDM=,iv:4uX7+Sl1UzgsImBH+qm9p8DlOfJu0i0W3bpoE7cFxXg=,tag:RROCnYBVKVwPBNSGgV30FQ==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:jpUzI7BmZA==,iv:PQrfm67la6RJCRq0H7qpiY8n2hPUjcaK6gmJ/zPQW30=,tag:bp/3GYbRpYOIub5nT0zz/A==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:fndXB4Y1,iv:EXOe0day1ScxvZ7ozn1QyHgaZgnvlPNmvSQG74aw7lM=,tag:quPmKvFTjRqtZoVUEoQAHQ==,type:str]
|
||||||
|
stringData:
|
||||||
|
CONSOLE_ACCESS_KEY: ENC[AES256_GCM,data:FhXl6yqTIzHsbJO6P96KQrA=,iv:gGTaSKxk721GwMQWWyymqUPZlGLidrBzWCWdRtnlOr0=,tag:IaVl8GQqPstBuUjD4N6JTQ==,type:str]
|
||||||
|
CONSOLE_SECRET_KEY: ENC[AES256_GCM,data:s22GOXY4B/m+S7tgDw8P2gU2F15vdn1tqAp53FWzFTI=,iv:GXBhHQ8YiaVjRRMw2yG4FxdulVOuOurkLXXC0u/SFRA=,tag:pvHcD9+4qaxnvhOSkNxoHA==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtUVJFQ1ErNmkzd1N6b29t
|
||||||
|
L0VWR2tlcEIxeG1CR1hXRkw2cEt0Q2crMWlVClg2UlNhUzdJZGpLZjJtYlAvbk16
|
||||||
|
eHk0ZHZiNzVRTWhyQzlieTU5Y3QraGsKLS0tIHA5UVhvUXhNSDk0QlV0R2RzR1dP
|
||||||
|
azVEQzgvSFNwak9TcW1USFNOb3RvSkUKNLuW3h3cHDSSCZGe9Vyiv5m+wXihtCYX
|
||||||
|
oJtbRK+kJF8oOUo4emI+SLGrCsWq5cG2wN/wgP+ChR1zUgRgfq5oOA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:hQfimKuhfiIiZQAyNDZQR25HZ5biU+kaWig3ElVPiqMY3B0LrmKyOEs7ittd3j3meUkPEFSmm1yYSXcdCuf6w7cwSbEiL5DG8t7QKPbnx/aRdyHAJW+1VC0e3Lye43gaHya0iUaWS/YFQcrT01oEbckLIDWPKfvA6pg2bZ4kMZ4=,iv:AydyWMVcjTQu+7Fzw5vtqkvTTeQEMt8y6snESoLUDE8=,tag:SoKklNqa9nH9G6d9+PnolQ==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:894=,iv:Swg6ADUgmrqwz7wqAZHip9/qwFu0Rn8S2Lx4gBH8LJM=,tag:zbv0tFfRLtwFxBfpsuLt8A==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:jzVtJHYw,iv:ToZ0orfJqfGF/OnAPeYu/g2f4fXAMOZQDkA1+tmIccs=,tag:pi2skbwIU8qOQVEC86MdAA==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:t7zCeZLvAEbkJqUjWi26yD6UDA==,iv:MS1gq/bwKZdLA1itVDtsrdSOfI7e2CrhjvX5yhs0eQA=,tag:lC1gcoFMI5nfzC56U1WXrg==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:tp+d,iv:gnlet/60mgbSWLXEQpSlcWD98ky7NvlNCzGLTMys0JQ=,tag:PQYj9UeA50YenQESTCl7lg==,type:str]
|
||||||
|
labels:
|
||||||
|
konghq.com/credential: ENC[AES256_GCM,data:SOqQ9bLGLK0=,iv:a7En49UhRDwgHbv5NRB/XilEYIKQdaDqKH86WDrJB5I=,tag:JaDZ6Ko8ovY6AZ1hW9YMGQ==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:r1K5gvop,iv:Gjv4oG2Unyql5rY9RTTljFqyd28xI81CDWtpavuuW5E=,tag:jF8lBs3AQwHPVEu9V+mONw==,type:str]
|
||||||
|
stringData:
|
||||||
|
key: ENC[AES256_GCM,data:pm9GmSvX5MAsXO/e6ZcI4NF1Hwr3qjG6LaEEjvV0ihvSWhO23drlAEsrTtzppqgDZbMORf+5+P53mXU=,iv:5AbHNKeiMPoFQP/qTKdA0vEoYPzuaf4kIdGGcMSmfIQ=,tag:LD5w7XK+hiCS5D410+nCfQ==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUTmoxQkpuYUp5UlRQMkph
|
||||||
|
a0RaazdvaU5sWkNuL2gvVjlUYXVWV0dUWVVRClZhWDZCN2hpS2hnOG9Pck9zOTkx
|
||||||
|
RGNGMEI3RHpNbzVaaWNGcTNSSEdzZHMKLS0tIFZERnVJWUpreUh3TTlwbGw0dUx4
|
||||||
|
MGlCSkxuWWVEK2RaSDZPUzhNSUlCa28KHN0IsgQc/kBqmjQ6+4sgfb9PJy/45MwN
|
||||||
|
rXaLJ1htpqPZ9MJ8iOukRi0IKnKgQWXsoZengIxGmcOnEctpoH/kyQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-14T01:07:50Z"
|
||||||
|
mac: ENC[AES256_GCM,data:nY+YVwU1GuK8Yz+EZOQKkZKN28tm2L8afflc6hsgVFCFmsep5kVT+zId7AgemvQ+qnrho5N5nqxY2knB0gusFfWNKF3V5A5GBq40WtZCMaAtcwhJSex4kK7ZyaZD6oWDW/RTUumSrivSowkWlqt1XlDKyFLqSlpWQTd7JiGmUv8=,iv:zE8+B5UVYSuuAGYyvXsAwp1N/4vGduCoGyEMCNNEUnM=,tag:6RcrjneV2dOzhmoQi+5HsA==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
apiVersion: viaduct.ai/v1
|
||||||
|
kind: ksops
|
||||||
|
metadata:
|
||||||
|
name: sops-secret-generator
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/function: |
|
||||||
|
exec:
|
||||||
|
path: ksops
|
||||||
|
files:
|
||||||
|
- agent-pod-models.enc.yaml
|
||||||
|
- agent-pod-ssh-key.enc.yaml
|
||||||
|
- authentik-secrets.enc.yaml
|
||||||
|
- cloudflare-secrets.enc.yaml
|
||||||
|
- forgejo-runner-token.enc.yaml
|
||||||
|
- forgejo-secrets.enc.yaml
|
||||||
|
- grafana-oidc-secrets.enc.yaml
|
||||||
|
- grafana-secrets.enc.yaml
|
||||||
|
- homarr-auth-oidc.enc.yaml
|
||||||
|
- homarr-db-encryption.enc.yaml
|
||||||
|
- homarr-secrets.enc.yaml
|
||||||
|
- homelab-ca-secrets.enc.yaml
|
||||||
|
- loki-secrets.enc.yaml
|
||||||
|
- model-invoke-apikey.enc.yaml
|
||||||
|
- minio-secrets.enc.yaml
|
||||||
|
- vault-secrets.enc.yaml
|
||||||
|
- vault-unseal-keys.enc.yaml
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:cM0=,iv:KFms9ElUhHn+w9flx5nzLfwvfJsMN0u8+YDMqFgB6V8=,tag:7Ve53FzTefhTYFVwnkT/iA==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:khv7iEsd,iv:rzn3Pt2pJcbDj55NYafUamNLTBrtluUPeLKorzpskGA=,tag:0PbEGGlxwz8ixBfwdOPi0w==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:jgWuWAVI4nQMy8dkJXZI5L4=,iv:f9pwyirRf/mLqr8LhuNPE53guP5mlZmPftNhtFKSbFA=,tag:YWX+qDU1z82FGSw/Wc3nJA==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:j77p,iv:rkU3kw12xPeVkVRn3w8WIZlc0+11jGeg1jZBX3kRfaQ=,tag:c2MrHHI4N8CeEQYDAr5wZg==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:F4hK9q+O,iv:7gmqUtqcYzDHp99fYdinL4yYSTvjoyHlTnhHbEynGQY=,tag:Yuw+B+h81bbZbWcd1zMSsA==,type:str]
|
||||||
|
stringData:
|
||||||
|
access_key: ENC[AES256_GCM,data:SFP7ZsrqSJXbaA==,iv:QuGF69veC8FWOSAbwzv7vhKYkF0yOp0ty/U0hdmroAA=,tag:5VNvfVONHySMbYcYnwSajw==,type:str]
|
||||||
|
secret_key: ENC[AES256_GCM,data:WxvqVZJWTckEb3paILxUJ51hkg0aNzRJzziuopsCpY/Amita3zXl91VJtGE=,iv:dyKnNC8255FCmOd61xczuk0Ww9MYZmHCDc+JY60AjIQ=,tag:rzem3XDOj7VGaYuhk0dc2g==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkaFFHVDhicUVPWEZHaXJn
|
||||||
|
eGc3WmdUYk9mdHp4RFExazRhWHBDa3VLN3gwCmZCUFBJaHRaSmtPRGhsQkRHSFJt
|
||||||
|
U2NUMUh4azR3NllLWDg5ek94ZlFSYU0KLS0tIE1BOU1IMVAveWtKaEE3QWhFeXp6
|
||||||
|
ZGd0TVZxbldTLzhHK1YrQUNCWUJVZlkKzK/aF0+qohgujyKHiRpQ+YYsnfVkvzDr
|
||||||
|
+3VnWcU1dPH63wqtf10LeYnBq8ewesfkibOVlJx0v1z6nQ9ZTMXwqQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:7JKz0tx066VLgwnpgA2/aUK/A7KD5y2PZJUqRNPMfEmEFqpkvm3Z5Ejj/kxmqYHwsXL+P92SrvMnbqq0v5I6PolR3fPW5fT/xcLibBmvoh8HF4wrRoaElK4ojTNDYJHginy/NE34FwL6sgDCbtT8QAJl9k8HEwxHlfBeyzzryaw=,iv:oTuLhckDQlvLHhGYNrpU0CSQB4Bunv2XvSsD53Z+yZQ=,tag:7CEOFC5t7vbFUQvencL/sQ==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:Gn8=,iv:DmWiGvlY/rBzp1Yh6yKw9pXjFO7smSB1fKfe7agSJz8=,tag:X6KSIbxlZWYYXKKxOfuThw==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:lsjz9hz4,iv:vLq5BRMjNRl2ZGDQSrgff7NiZQsBEMSl6OTZ5dAMisk=,tag:ug62YjRZXbN1Y7PoMihsIw==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:TkkvNs86o2+hC8MAi8lEeU8=,iv:SaxeZgpWekHMJeoHKaoZYGPEVGw42XUk/sT6SaKGVN0=,tag:mtA9SwNKiXh3NrC5rdasRg==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:wK6m,iv:KtA31Bo8aGE1HU8H9KWbMwt2NfywWfy77G/LaReaI1E=,tag:V9SSlPawRQw0x2utFNN1aw==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:6UPZ1ZTR,iv:FxN1ebrlJ4IO3eDGEYSvktSpMgAebcl0DO1WHh5O0+0=,tag:2bZN4zdbHzx0oR+bTJPPJg==,type:str]
|
||||||
|
stringData:
|
||||||
|
key1: ENC[AES256_GCM,data:dV2HOh7W1Pl0QDJaGvtEKpBppybQMiK5kxzyk05zkA352ZmJvE8Ppm5yWKk=,iv:grQ8v2o/LHpJnIZonjtgTHKcLUQIH8xl1FtWtEs4rEs=,tag:lANcccYT6Ouo/0IcoLw4uA==,type:str]
|
||||||
|
key2: ENC[AES256_GCM,data:YSdpYL8h56PfUrvMFhBXhmBG2en0toLKEpYlJqwjAk/vI0jm7gq/TqGn034=,iv:GxthkNLhm3qHxjkZetiYl58Qa/K7G2ib6E+LWK16H8Q=,tag:q79UUoh6uCcMZJ0U18Ireg==,type:str]
|
||||||
|
key3: ENC[AES256_GCM,data:sO7Lao9qkmIOdARL+6FBcLo+e4z8LzMEzNrwcy5hv9uRuxppwAXrP0MO1vw=,iv:wSWe9h1HovRhV5YCZrmvcB2VEfRYQ+gasGO3uh8IpmQ=,tag:Bc5yaROQDTz1AhVnFDm/BA==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtekFMUWlnb2tiUVhCODE1
|
||||||
|
TkNCclVTa0dVUFhlc0l2elUrVi9MUytSM3pRCkdtYlRFL1cwTHNPZnFicHZwUnZU
|
||||||
|
eENhS0dIek1JbzZEODFSY2tyem91MUUKLS0tIFIzQzNmcERZM0hBbGI0TndEeGlB
|
||||||
|
MFk1RTBFbHRqempyYlQ2U05YVko2R1EKVvk1Rd9ZU0G1GHX+3mmlHAfQOPzmoPsk
|
||||||
|
7RltNZF2SCxjIv5C2pqf3CgmRBaQGWgMybRRH5gdB87PLBKkPL3+HQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T20:52:24Z"
|
||||||
|
mac: ENC[AES256_GCM,data:kLjoBWmJ2bZ13EbuzgppahHKmCY/xtCi1R7xCEUbCP4FQfiVaC4qAbVftE4lTNlXa2mqCKsGPYZ/A3HV/4+a+it/pg0a+rj7E7DszhieWbZufHMJs+w4/Le8l8wFFE5aNW0wdvGTJZ0n4HBOdAkE1qN9ReaiaQgr0rk5ggOPG9g=,iv:MiDVUgcoKrP/gj4keqomo6OB12dmz9VoesTiHQTnBFM=,tag:cLK67Pym4BuvXLF1RUA8Gw==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
apiVersion: ENC[AES256_GCM,data:AYk=,iv:/1G8RSCBaiQ6msQEPPOO4LzlbYOzEbNvT9OwnY0E1JQ=,tag:Gq2I92mI3c1FFsbv2ZE+BQ==,type:str]
|
|
||||||
kind: ENC[AES256_GCM,data:PbHIh8xS,iv:KMW7WL9ZjTmUz3NOQRhjsuAH2EZFXGk1P4hhl7cp8c0=,tag:9vJiqOtV3EhVfXWjJr4ZhA==,type:str]
|
|
||||||
metadata:
|
|
||||||
name: ENC[AES256_GCM,data:mF7O9mz8+KGfU6DJ8ifP974at95QOPH7yIaC,iv:1qXckUq4RTBZ06YNLXsuMW1UWRK9cgymDdVnmwLZles=,tag:v2dzEB+J23h4MxxkcJThIg==,type:str]
|
|
||||||
namespace: ENC[AES256_GCM,data:7omPsuwSfF1IwvCN,iv:YCv7jIXpkzTx9iu9ScwHme8Nc8iqJr1xn8yXK/OsHk8=,tag:5zslu/jpTpMHi2XvCP92Ig==,type:str]
|
|
||||||
type: ENC[AES256_GCM,data:nF1pv2+3,iv:iseIGnyGeVE76BORrjX/TnvrfzaRMOFrQ7oWT0jIS9M=,tag:qTpF2hYVmnCwL8EQA9Ot8w==,type:str]
|
|
||||||
stringData:
|
|
||||||
api-token: ENC[AES256_GCM,data:Evzvkz7ywKLsTXA0/ZF09Y+uKWDQ16SbAsogo4t9aAd+zsvFAnsyVifQc0SUNxzjSv8FXws=,iv:uFvlesWdZUbiuh8WTq1uLBRtuptV2tXxGJ6PVgu7vCI=,tag:thpyhOHtKWNn0bi1Xi1q6w==,type:str]
|
|
||||||
sops:
|
|
||||||
age:
|
|
||||||
- enc: |
|
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwM0ovVUZmZVFmbjlwOXgx
|
|
||||||
U2h6aDNVVmV6NC8xckZtN3lwVytneHZvQ0VFCld3YjlhTFU5aVd0T3NpOUU3NW16
|
|
||||||
ak10VVFiMnNsNG9GS0tIN1ViZjRrcG8KLS0tIFNScE5ibkYvQWszeDl2UkF2VHpG
|
|
||||||
Rzd2NlNSYlRYY0pkaDY1YklGS1NaWFUKTmYqAOF+iPbgl8Qv1qEyGLSDn3Yqw3Sd
|
|
||||||
iYpwVUPMrrLG9FiaSTUavmkv96DCEnof3GStaYsd62MXFWHW9eFAtA==
|
|
||||||
-----END AGE ENCRYPTED FILE-----
|
|
||||||
recipient: age1smu533f803gmd0jq60s2zaj9zlznajy0ca6rtewd4r37mr2hs3uqsrldfh
|
|
||||||
lastmodified: "2026-07-20T19:49:33Z"
|
|
||||||
mac: ENC[AES256_GCM,data:6d8l+mal0zlwSyK1zkvpE8Y2cknXM+deaiIdzc2nySEm2pkC2ryY6exmPOsiyjDFrH9vnvwpvWxUqOZc1GPJ7/Pe6lDsdgT7ZPKrzJwfG07lXCZTNMTKruQ745mPgMzoJ82OjC8dXrXRXAqbFMw7SJeCajlfwL2Odmqr0reTnYs=,iv:FEcmRP5KYbI6QBZDLvcgF4ko4g5j2I8+eFrzPay80fw=,tag:cdUfwl+8Zwjyvu/p8htS1w==,type:str]
|
|
||||||
unencrypted_suffix: _unencrypted
|
|
||||||
version: 3.13.2
|
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
# Public root cert for the self-signed homelab-ca, distributed so internal
|
||||||
|
# services (Authentik, forgejo-runner, management-service, blackbox-exporter)
|
||||||
|
# can trust certs cert-manager issues off the homelab-ca ClusterIssuer.
|
||||||
|
# Not sensitive — public cert only, no private key. One copy per consuming
|
||||||
|
# namespace since ConfigMaps aren't cluster-scoped.
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: homelab-ca
|
||||||
|
namespace: iam
|
||||||
|
data:
|
||||||
|
ca.crt: |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
homelab-ca.crt: |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: homelab-ca
|
||||||
|
namespace: cicd
|
||||||
|
data:
|
||||||
|
ca.crt: |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
homelab-ca.crt: |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: homelab-ca
|
||||||
|
namespace: monitoring
|
||||||
|
data:
|
||||||
|
ca.crt: |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
homelab-ca.crt: |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFBjCCAu6gAwIBAgIJAKoBPdKPDL5AMA0GCSqGSIb3DQEBCwUAMDAxEzARBgNV
|
||||||
|
BAMMCmhvbWVsYWItY2ExGTAXBgNVBAoMEHJpb3RwaWFvLmhvbWVsYWIwHhcNMjYw
|
||||||
|
ODEyMjA1NDQzWhcNMzYwODA5MjA1NDQzWjAwMRMwEQYDVQQDDApob21lbGFiLWNh
|
||||||
|
MRkwFwYDVQQKDBByaW90cGlhby5ob21lbGFiMIICIjANBgkqhkiG9w0BAQEFAAOC
|
||||||
|
Ag8AMIICCgKCAgEAzw796vZRFOBc/orUu9J6rv91A3SNLJsqz/VuetwAXCaVSlK5
|
||||||
|
kL/RaBKKkcXToCC5avwix6W21FnwLUW4R8QlQdin8+XEHpx2LY5R4GmSvaCbU7wm
|
||||||
|
YwYgVGQVv8KlgcMbz/pF09vXU29cHUHwlkN+BWVGAxVGWxam94j1pBzV0dOhhRw2
|
||||||
|
q5sZYUUrPO5l775od2FMMnMzoRFugxDvJof0p3fAChNAIiFhP95lrcb2YsTwK+a2
|
||||||
|
6mk3AZc0gMtmgVsH+zryGFcAmWLixq41ye/H23ktj2bjEHCr9mIDpuSOKLyLBzrg
|
||||||
|
js/HqdxvwkZkkxwXft0Vi7GN0/LGHMdPrDE499pJelAHbxUpBoOk8sfXSNnfESI8
|
||||||
|
v20LybATGrKXFlOhMKshY1a3JLAoZTaW0xkG4PM4HT7lK/ovr/NLF6kdP7HxJFFQ
|
||||||
|
+4WjySiwbjusNobpTaQWVwCjD3Imf+2feAqJeX3z+P3EhxfrmvEojLOdUiiPcjMn
|
||||||
|
CFl9u2EpSlax53umSkcBQJGfFKM17fZzhtakjqhCTzAfXXlxZy4hsHDtfzRcfaoB
|
||||||
|
Zz/PiJ0TtJSC7TvxZZoPpGexQdoeT7kJEksTvGi5OSfJD7HaKe5RQSZGgpz7ATB+
|
||||||
|
YMBKbjhMXVu+TAdcO1qZRN0UCncrliN61ki9F0tE8kNgpS9biZ6kheoJMZUCAwEA
|
||||||
|
AaMjMCEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcN
|
||||||
|
AQELBQADggIBAFN1At11d/+Ue6cEjCklnRQpLuD5agXaE+pi5oHNfnggSlO8UjUj
|
||||||
|
lDzfnmxi3IAI6HruzDARM9J95qJboUxWOAEmK9SNnZNS/1b0GyikP3YcF1gIFqtW
|
||||||
|
6SgmVfC1cVUFKGt/+QQzZxhOLuoydv9Ozu12nxsTW/WY2t+cpeL+1wrsM22RA6W/
|
||||||
|
xV/bzYbwzCUT9v4tMZLZHk5CkXztVFXSJyW/8w9rVZdUGZM+mwwn81PLSE0v3s/2
|
||||||
|
wJH8eUqGl73i3Xgn7CVanN4cJYMGYVVlRD6Qr6muBN4Vh0ETY1PrjzsZ6XsV6vk9
|
||||||
|
glMbQnErYMZHZIDtnVdaBt60MecUD9O4eLeip0SE6bVRAucKBXA9xNMilMQEL8/X
|
||||||
|
FBMAxXl10rowBlbX2oaFSOM80eLTla4a3gSL7+0vua3C4Dtv2bLo8ZjCyqXwOJeb
|
||||||
|
N85cqcVZStz3qlgQN1nG2qGE3BS6scOs7+mUGe9CmS8acJV++PKm8CtmLH6/ibjV
|
||||||
|
tAc/6+Vg/keo0kty7H7L6oQd+gKpuaJ2I+wKYe4l9UwHi9zEb8YjzGB4n2AtsoWI
|
||||||
|
NDKi7/Gxha790QN3TD/U7p9GcEtfXKv8KISKfxnfD9o9Yx6pWOXcgvGefQkpXuRm
|
||||||
|
kGzDM+aY/VKs5Et5/RyKavBDihuOcumLsmoRg/bFoB/xpeURTyiehUdu
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: homelab-ca
|
||||||
|
namespace: sqs
|
||||||
|
data:
|
||||||
|
ca.crt: |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
homelab-ca.crt: |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# k8s/bootstrap/cert-manager/homelab-ca-issuer.yaml
|
||||||
|
# Self-signed internal CA, for services that only need to trust each other
|
||||||
|
# on the homelab LAN (not exposed to the internet, no public ACME needed).
|
||||||
|
# Root cert+key live in homelab-ca-secrets.enc.yaml (cert-manager namespace).
|
||||||
|
# The public cert is separately distributed via homelab-ca-configmap.yaml so
|
||||||
|
# non-cert-manager pods (Authentik, forgejo-runner, etc.) can trust it too.
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: homelab-ca
|
||||||
|
spec:
|
||||||
|
ca:
|
||||||
|
secretName: homelab-ca-keypair
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
namespace: cert-manager
|
# Issuers + CA trust-bundle ConfigMaps for the cert-manager-issuers Application.
|
||||||
resources: []
|
# Each resource sets its own metadata.namespace (the ConfigMaps target iam/cicd/
|
||||||
# cert-manager deployed via ArgoCD Helm source (see layer-2-bootstrap app)
|
# monitoring/sqs; ClusterIssuers are cluster-scoped) — so NO namespace transformer
|
||||||
|
# here (that would rewrite them all into one namespace). cert-manager itself is a
|
||||||
|
# separate Helm Application; cert-manager-values.yaml here is only its $values ref.
|
||||||
|
resources:
|
||||||
|
- letsencrypt-issuer.yaml
|
||||||
|
- homelab-ca-issuer.yaml
|
||||||
|
- homelab-ca-configmap.yaml
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
namespace: kube-system
|
|
||||||
resources: []
|
|
||||||
# Cilium deployed via Helm chart
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# k8s/cilium/l2-announcement-policy.yaml
|
|
||||||
# CiliumL2AnnouncementPolicy — without this, LB-IPAM (lb-ipam-pool.yaml)
|
|
||||||
# assigns IPs to LoadBalancer Services but nothing ARPs for them on the LAN,
|
|
||||||
# so they're unreachable from outside the cluster even though `kubectl get
|
|
||||||
# svc` shows a real EXTERNAL-IP. Confirmed both forgejo's .165 and
|
|
||||||
# shadowsocks' .166 were 100% packet loss / incomplete ARP before this.
|
|
||||||
#
|
|
||||||
# loadBalancerIPs: true makes Cilium announce every Service's LB-IPAM IP via
|
|
||||||
# ARP from whichever node currently holds the lease for it (one node per IP,
|
|
||||||
# decided by leaderElection — not all nodes simultaneously, which would
|
|
||||||
# otherwise cause ARP flapping/duplicate-IP confusion on the LAN).
|
|
||||||
#
|
|
||||||
# externalIPs/loadBalancerIPs split exists because Cilium also supports
|
|
||||||
# announcing Service externalIPs (a different field, unused in this repo);
|
|
||||||
# we only need loadBalancerIPs since every exposed Service here is type
|
|
||||||
# LoadBalancer via lb-ipam-pool.yaml.
|
|
||||||
#
|
|
||||||
# requires kube-proxy replacement (already the case — see
|
|
||||||
# k8s/talos-iam or helmfile.yaml.gotmpl kubeProxyReplacement=true) and a
|
|
||||||
# Cilium build with L2 announcements enabled (default since v1.14).
|
|
||||||
#
|
|
||||||
# Apply once after cluster bootstrap, alongside lb-ipam-pool.yaml:
|
|
||||||
# kubectl apply -f k8s/cilium/l2-announcement-policy.yaml
|
|
||||||
#
|
|
||||||
# Verify:
|
|
||||||
# kubectl get ciliuml2announcementpolicy
|
|
||||||
# ping 192.168.1.165 && ping 192.168.1.166 # both should now respond
|
|
||||||
# arp -a | grep 192.168.1.16 # should resolve to a real MAC
|
|
||||||
apiVersion: cilium.io/v2alpha1
|
|
||||||
kind: CiliumL2AnnouncementPolicy
|
|
||||||
metadata:
|
|
||||||
name: homelab-l2-announce
|
|
||||||
spec:
|
|
||||||
loadBalancerIPs: true
|
|
||||||
interfaces:
|
|
||||||
- eno1
|
|
||||||
# No nodeSelector restriction — all 3 nodes already run workloads
|
|
||||||
# (allowSchedulingOnControlPlanes: true in controlplane.yaml), and with
|
|
||||||
# 3 zone-labeled nodes, redundancy for per-IP leader election is maintained.
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# k8s/cilium/lb-ipam-pool.yaml
|
|
||||||
# CiliumLoadBalancerIPPool — tells Cilium LB-IPAM which IPs it can assign
|
|
||||||
# to LoadBalancer services in this cluster.
|
|
||||||
#
|
|
||||||
# CIDR 192.168.1.160/28 covers .160–.175 on the LAN:
|
|
||||||
# .160 talos-cp-1 (node — not assignable to services)
|
|
||||||
# .161 reserved
|
|
||||||
# .162 talos-worker-1 (node — not assignable to services)
|
|
||||||
# .163–.175 free for LoadBalancer services
|
|
||||||
#
|
|
||||||
# Current service IP assignments (via io.cilium/lb-ipam-ips annotation):
|
|
||||||
# 192.168.1.165 forgejo-gitea-http (cicd)
|
|
||||||
# 192.168.1.165 forgejo-gitea-ssh (cicd) — same IP, different ports
|
|
||||||
# 192.168.1.166 shadowsocks (vpn)
|
|
||||||
#
|
|
||||||
# Apply once after cluster bootstrap:
|
|
||||||
# kubectl apply -f k8s/cilium/lb-ipam-pool.yaml
|
|
||||||
#
|
|
||||||
# Verify assignment:
|
|
||||||
# kubectl get svc -n cicd forgejo-gitea-http forgejo-gitea-ssh
|
|
||||||
# # EXTERNAL-IP should change from <pending> to 192.168.1.165
|
|
||||||
|
|
||||||
apiVersion: "cilium.io/v2alpha1"
|
|
||||||
kind: CiliumLoadBalancerIPPool
|
|
||||||
metadata:
|
|
||||||
name: homelab-pool
|
|
||||||
spec:
|
|
||||||
blocks:
|
|
||||||
- cidr: "192.168.1.160/28"
|
|
||||||
# DO NOT add any 10.6.0.0/24 block here. That is the WireGuard subnet
|
|
||||||
# (10.6.0.1 = talos-cp-1 tunnel IP, 10.6.0.2 = DNS — see
|
|
||||||
# cluster-config/controlplane.yaml). A 10.6.0.x block let Cilium LB-IPAM
|
|
||||||
# auto-assign the CP's own tunnel IP to a Service, which broke the
|
|
||||||
# WireGuard tunnel and locked out the default kubectl context. It also
|
|
||||||
# can't work over WireGuard anyway — L2 announcements only ARP on eno1
|
|
||||||
# (the LAN interface), not wg0. Keep this pool LAN-only.
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
# k8s/coredns/coredns-configmap.yaml
|
|
||||||
# Patches the CoreDNS Corefile to rewrite homelab hostnames to internal services.
|
|
||||||
#
|
|
||||||
# Why this is needed:
|
|
||||||
# Grafana v10+ does OIDC auto-discovery by fetching
|
|
||||||
# /.well-known/openid-configuration from Authentik. When Grafana reaches
|
|
||||||
# Authentik via the external hostname (authentik.riotpiao.com), the
|
|
||||||
# HTTP Host header is preserved and Authentik returns external URLs in the
|
|
||||||
# discovery response. Without this rewrite, the hostname doesn't resolve
|
|
||||||
# inside the cluster and Grafana falls back to the internal service DNS,
|
|
||||||
# causing all OAuth redirects to go to authentik-server.iam.svc.cluster.local.
|
|
||||||
#
|
|
||||||
# Applied by helmfile presync hook on the ingress-nginx release.
|
|
||||||
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: coredns
|
|
||||||
namespace: kube-system
|
|
||||||
data:
|
|
||||||
Corefile: |
|
|
||||||
.:53 {
|
|
||||||
errors
|
|
||||||
health {
|
|
||||||
lameduck 5s
|
|
||||||
}
|
|
||||||
ready
|
|
||||||
log . {
|
|
||||||
class error
|
|
||||||
}
|
|
||||||
prometheus :9153
|
|
||||||
|
|
||||||
# VPN Access: Map api-server.cluster.local to cluster API IP
|
|
||||||
# Allows secure cluster access via WireGuard tunnel (Shadowrocket/Talos)
|
|
||||||
rewrite name api-server.cluster.local kubernetes.default.svc.cluster.local
|
|
||||||
|
|
||||||
# Forgejo: route through nginx ingress like every other host below. nginx
|
|
||||||
# terminates TLS (wildcard-tls) on :443 and routes both /v2/ (container
|
|
||||||
# registry) and web/git to forgejo-gitea-http:3000.
|
|
||||||
# Do NOT point this at forgejo-gitea-http directly: that service only serves
|
|
||||||
# port 3000, so containerd image pulls (which use https/:443) get
|
|
||||||
# `dial tcp <clusterIP>:443: i/o timeout`. SSH stays on its own LB service.
|
|
||||||
rewrite name forgejo.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
|
||||||
rewrite name git.riotpiao.com forgejo-gitea-ssh.cicd.svc.cluster.local
|
|
||||||
|
|
||||||
# Rewrite homelab hostnames to the nginx ingress controller so in-cluster pods
|
|
||||||
# hit nginx TLS termination (cert-manager cert) and preserve the Host header.
|
|
||||||
# Routing through nginx — not directly to the backend service — is critical:
|
|
||||||
# direct rewrites to the backend bypass nginx TLS and expose each app's own
|
|
||||||
# self-signed cert, which nothing in the cluster trusts.
|
|
||||||
rewrite name authentik.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
|
||||||
rewrite name grafana.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
|
||||||
rewrite name minio.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
|
||||||
rewrite name minio-api.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
|
||||||
rewrite name argocd.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
|
||||||
rewrite name vault.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
|
||||||
rewrite name loki.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
|
||||||
rewrite name prometheus.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
|
||||||
rewrite name homarr.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
|
||||||
rewrite name portainer.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
|
||||||
rewrite name longhorn.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
|
||||||
|
|
||||||
kubernetes cluster.local in-addr.arpa ip6.arpa {
|
|
||||||
pods insecure
|
|
||||||
fallthrough in-addr.arpa ip6.arpa
|
|
||||||
ttl 30
|
|
||||||
}
|
|
||||||
forward . 8.8.8.8 1.1.1.1 {
|
|
||||||
max_concurrent 1000
|
|
||||||
}
|
|
||||||
cache 30 {
|
|
||||||
disable success cluster.local
|
|
||||||
disable denial cluster.local
|
|
||||||
}
|
|
||||||
loop
|
|
||||||
reload
|
|
||||||
loadbalance
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
# CoreDNS is deployed by Talos's bootstrap manifests (not a helm release we
|
|
||||||
# own). This tracks the full Deployment spec as the single source of truth
|
|
||||||
# for any changes we apply on top of the Talos default — currently just
|
|
||||||
# topologySpreadConstraints so the 2 replicas don't land on the same node.
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: coredns
|
|
||||||
namespace: kube-system
|
|
||||||
labels:
|
|
||||||
k8s-app: kube-dns
|
|
||||||
kubernetes.io/name: CoreDNS
|
|
||||||
spec:
|
|
||||||
replicas: 2
|
|
||||||
revisionHistoryLimit: 10
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
k8s-app: kube-dns
|
|
||||||
strategy:
|
|
||||||
rollingUpdate:
|
|
||||||
maxSurge: 25%
|
|
||||||
maxUnavailable: 1
|
|
||||||
type: RollingUpdate
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
k8s-app: kube-dns
|
|
||||||
spec:
|
|
||||||
affinity:
|
|
||||||
podAntiAffinity:
|
|
||||||
preferredDuringSchedulingIgnoredDuringExecution:
|
|
||||||
- podAffinityTerm:
|
|
||||||
labelSelector:
|
|
||||||
matchExpressions:
|
|
||||||
- key: k8s-app
|
|
||||||
operator: In
|
|
||||||
values:
|
|
||||||
- kube-dns
|
|
||||||
topologyKey: kubernetes.io/hostname
|
|
||||||
weight: 100
|
|
||||||
containers:
|
|
||||||
- args:
|
|
||||||
- -conf
|
|
||||||
- /etc/coredns/Corefile
|
|
||||||
env:
|
|
||||||
- name: GOMEMLIMIT
|
|
||||||
value: 161MiB
|
|
||||||
image: registry.k8s.io/coredns/coredns:v1.14.2
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
livenessProbe:
|
|
||||||
failureThreshold: 5
|
|
||||||
httpGet:
|
|
||||||
path: /health
|
|
||||||
port: 8080
|
|
||||||
scheme: HTTP
|
|
||||||
initialDelaySeconds: 60
|
|
||||||
periodSeconds: 10
|
|
||||||
successThreshold: 1
|
|
||||||
timeoutSeconds: 5
|
|
||||||
name: coredns
|
|
||||||
ports:
|
|
||||||
- containerPort: 53
|
|
||||||
name: dns
|
|
||||||
protocol: UDP
|
|
||||||
- containerPort: 53
|
|
||||||
name: dns-tcp
|
|
||||||
protocol: TCP
|
|
||||||
- containerPort: 9153
|
|
||||||
name: metrics
|
|
||||||
protocol: TCP
|
|
||||||
readinessProbe:
|
|
||||||
failureThreshold: 3
|
|
||||||
httpGet:
|
|
||||||
path: /ready
|
|
||||||
port: 8181
|
|
||||||
scheme: HTTP
|
|
||||||
periodSeconds: 10
|
|
||||||
successThreshold: 1
|
|
||||||
timeoutSeconds: 1
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 170Mi
|
|
||||||
requests:
|
|
||||||
cpu: 100m
|
|
||||||
memory: 70Mi
|
|
||||||
securityContext:
|
|
||||||
allowPrivilegeEscalation: false
|
|
||||||
capabilities:
|
|
||||||
add:
|
|
||||||
- NET_BIND_SERVICE
|
|
||||||
drop:
|
|
||||||
- ALL
|
|
||||||
readOnlyRootFilesystem: true
|
|
||||||
terminationMessagePath: /dev/termination-log
|
|
||||||
terminationMessagePolicy: File
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /etc/coredns
|
|
||||||
name: config-volume
|
|
||||||
readOnly: true
|
|
||||||
dnsPolicy: Default
|
|
||||||
nodeSelector:
|
|
||||||
kubernetes.io/os: linux
|
|
||||||
priorityClassName: system-cluster-critical
|
|
||||||
restartPolicy: Always
|
|
||||||
schedulerName: default-scheduler
|
|
||||||
serviceAccount: coredns
|
|
||||||
serviceAccountName: coredns
|
|
||||||
terminationGracePeriodSeconds: 30
|
|
||||||
tolerations:
|
|
||||||
- effect: NoSchedule
|
|
||||||
key: node-role.kubernetes.io/control-plane
|
|
||||||
operator: Exists
|
|
||||||
- effect: NoSchedule
|
|
||||||
key: node.cloudprovider.kubernetes.io/uninitialized
|
|
||||||
operator: Exists
|
|
||||||
topologySpreadConstraints:
|
|
||||||
- labelSelector:
|
|
||||||
matchLabels:
|
|
||||||
k8s-app: kube-dns
|
|
||||||
maxSkew: 1
|
|
||||||
topologyKey: kubernetes.io/hostname
|
|
||||||
whenUnsatisfiable: ScheduleAnyway
|
|
||||||
volumes:
|
|
||||||
- configMap:
|
|
||||||
defaultMode: 420
|
|
||||||
items:
|
|
||||||
- key: Corefile
|
|
||||||
path: Corefile
|
|
||||||
name: coredns
|
|
||||||
name: config-volume
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
namespace: kube-system
|
|
||||||
resources: []
|
|
||||||
# CoreDNS deployed via Helm chart
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# Service alias for CoreDNS compatibility
|
|
||||||
# CoreDNS rewrites *.riotpiao.com → ingress-nginx-controller.ingress-nginx.svc
|
|
||||||
# But bootstrap deployed as ingress-nginx-bootstrap-controller
|
|
||||||
# This alias makes both names work
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: ingress-nginx-controller
|
|
||||||
namespace: ingress-nginx
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: ingress-nginx
|
|
||||||
app.kubernetes.io/instance: ingress-nginx-bootstrap
|
|
||||||
app.kubernetes.io/component: controller
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 80
|
|
||||||
targetPort: http
|
|
||||||
- name: https
|
|
||||||
port: 443
|
|
||||||
targetPort: https
|
|
||||||
@@ -300,6 +300,10 @@ spec:
|
|||||||
port:
|
port:
|
||||||
number: 8080
|
number: 8080
|
||||||
---
|
---
|
||||||
|
# NOTE: api.riotpiao.com (Kong) is deliberately NOT here. Its namespace `api` is
|
||||||
|
# created in wave 7, and this Application syncs in wave 1 — an Ingress into a
|
||||||
|
# namespace that doesn't exist yet would fail and mark this whole app
|
||||||
|
# SyncFailed. It lives in k8s/apps/api/ingress.yaml, synced with Kong itself.
|
||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: Ingress
|
kind: Ingress
|
||||||
metadata:
|
metadata:
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ apiVersion: kustomize.config.k8s.io/v1beta1
|
|||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
# No top-level namespace - resources declare their own namespaces
|
# No top-level namespace - resources declare their own namespaces
|
||||||
resources:
|
resources:
|
||||||
- ingress-nginx-controller-alias.yaml # Service alias for CoreDNS compatibility
|
# ingress-nginx-controller-alias.yaml REMOVED — it was a ClusterIP Service named
|
||||||
|
# ingress-nginx-controller with a stale selector (instance: ingress-nginx-bootstrap,
|
||||||
|
# a release that no longer exists). ingress-config's selfHeal kept re-applying it
|
||||||
|
# over the helm release's real LoadBalancer Service of the same name, reverting it
|
||||||
|
# to a ClusterIP with zero endpoints -> LB IP .160 unannounced -> cluster-wide
|
||||||
|
# outage. CoreDNS rewrites *.riotpiao.com to ingress-nginx-controller.ingress-nginx
|
||||||
|
# .svc, which is the helm Service directly — no alias needed.
|
||||||
- riotpiao-com-cert.yaml # Certificate for *.riotpiao.com (ingress-nginx namespace)
|
- riotpiao-com-cert.yaml # Certificate for *.riotpiao.com (ingress-nginx namespace)
|
||||||
- ingress.yaml # Ingress rules for all services (multiple namespaces)
|
- ingress.yaml # Ingress rules for all services (multiple namespaces)
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
#ENC[AES256_GCM,data:85lNBL02TeroUW8dissgxEwjnOZnb0fg7jndGim3/QSi3/hFEF8FQa9nrQ==,iv:lM1jBHIxFkQriZ6BjGRXAlymUs5nX4Kvt1WNxUeZlkU=,tag:wm/D5qVcrcu3VXcZkEj70A==,type:comment]
|
|
||||||
apiVersion: ENC[AES256_GCM,data:Vrs=,iv:0TzPcIoozs2MXJNXkzgcVtjjBUgfOHaSXQZiD37fb+Q=,tag:CabTlLwtz6RcBF/gr4Ri4g==,type:str]
|
|
||||||
kind: ENC[AES256_GCM,data:22Y5w+Df,iv:Mf2s3h8++Vxqb4JoymHXY4/WAknDZ2GGrVVtKK51JxI=,tag:k5DHUqWGMBPpLkQXADaMlw==,type:str]
|
|
||||||
metadata:
|
|
||||||
name: ENC[AES256_GCM,data:ItXKquY5a7gu8CJZog==,iv:jxbj7Qtv+DRbhzTdvtv+eJuTQPNIf497NZPYA6ld4s0=,tag:XdaaSavFBxOjvxna2kr/Tg==,type:str]
|
|
||||||
namespace: ENC[AES256_GCM,data:VH6NMg==,iv:4PfWZu5qVGXP3ZzRHMrh5N9dzJ3SoUPPo58ppcDTnpk=,tag:Vr8E+WF0Z9ym3GyTGbdi2g==,type:str]
|
|
||||||
type: ENC[AES256_GCM,data:DmbHZRIk,iv:EZHnf1h1L29G1HOBYBSBeydNe4nC8XiBOw8YEL3kxrY=,tag:pFhUSMAIEqJET3NmaajO1g==,type:str]
|
|
||||||
stringData:
|
|
||||||
username: ENC[AES256_GCM,data:ZTrmFA==,iv:1+tLTAxrDitXJwCAEccaVQzc9I9lNRgT3FsxO2NPDDc=,tag:69Wn4a8pjhdUycc/MXsn3Q==,type:str]
|
|
||||||
password: ENC[AES256_GCM,data:w7Vn8XaC1ykNrwPpJjVYg8J5KXkUbaPspu0CoceqHVdai6BFNW5rtA==,iv:jAwDFvJfQ1GkeU/qpEVUAQ6cWqxYE8nrgs+/RouyUxg=,tag:foGmYika/9uNBdsKVrspEA==,type:str]
|
|
||||||
email: ENC[AES256_GCM,data:pIOq80OjVERFCXJmx8+qTJEw,iv:mfkMj3u8W2ZX4N4IH39mZXfEp+xphS4shaJcFOD/LEE=,tag:FOSq0E7VzzOM69Bn77v2Dw==,type:str]
|
|
||||||
sops:
|
|
||||||
age:
|
|
||||||
- enc: |
|
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwSi84U3FQZXJrYzA0WCtS
|
|
||||||
c1g1Sm04eFhha2huSEVCd2x3OGVUT08yejFjCmpPV0NsTVRyWTRVUkx6WktKaU1k
|
|
||||||
Vk12VGRIejRWbjQxeDZxdFUxWkgzaVUKLS0tIDRZdFNxY1AvcFhOREMxU01zTzhX
|
|
||||||
RzFmMjdjOFd4ZkxscnBIa1E2NWtRaVEK7qZXtq2VwZBwsLAulRh93TpCXCos0Vu7
|
|
||||||
fX+/oMEN6gF5VxDJ/e5C644EKUY+tSLAoh75xA5DAytOxEVdhe2ozA==
|
|
||||||
-----END AGE ENCRYPTED FILE-----
|
|
||||||
recipient: age1smu533f803gmd0jq60s2zaj9zlznajy0ca6rtewd4r37mr2hs3uqsrldfh
|
|
||||||
lastmodified: "2026-07-25T17:45:22Z"
|
|
||||||
mac: ENC[AES256_GCM,data:gi8smimG00EQbF492BVzsWOEubGdT0zSG39D+r3fgLL/wgtDqF2mgtFA0iDiy1pInt1Viu8yYpu1l/eBSD8BNW4bfrglIHOJdb1I5+19u8SMebhWCwbEcbiHaXNmVUKKapGsNjbApCApq00NXoOZuOUSUzZlh19JPvg/vd7nl2M=,iv:n8Kni4RQhj/VgIruqptVa6m0YwFJF6v2+p2boJsLT5c=,tag:Tc4gkhH7hSrJKEOJ3X3SjQ==,type:str]
|
|
||||||
unencrypted_suffix: _unencrypted
|
|
||||||
version: 3.13.2
|
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
# Forgejo Helm Values — Single Source of Truth
|
# Forgejo Helm Values — Single Source of Truth
|
||||||
# Chart: https://codeberg.org/forgejo-contrib/forgejo-helm
|
# Chart: https://codeberg.org/forgejo-contrib/forgejo-helm
|
||||||
|
|
||||||
|
# Recreate (not RollingUpdate): the gitea data volume is a single RWO PVC. With
|
||||||
|
# RollingUpdate the new pod tries to attach the PVC while the old pod still holds
|
||||||
|
# it -> "Multi-Attach error", new pod stuck Init forever, rollout wedged. Recreate
|
||||||
|
# terminates the old pod first so the PVC detaches before the new one starts.
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
|
||||||
# Disable bundled dependencies (use external CNPG + Redis instead)
|
# Disable bundled dependencies (use external CNPG + Redis instead)
|
||||||
postgresql-ha:
|
postgresql-ha:
|
||||||
enabled: false
|
enabled: false
|
||||||
@@ -14,6 +21,18 @@ valkey-cluster:
|
|||||||
redis:
|
redis:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
|
# External SSH access for git over the LAN. The chart's ssh Service becomes a
|
||||||
|
# LoadBalancer with a stable IP from the Cilium homelab-pool (192.168.1.160/28,
|
||||||
|
# L2-announced) so `git clone ssh://[email protected]:2222/...` works from the
|
||||||
|
# LAN. gitea's sshd listens on 2222 in-pod; port 2222 is exposed directly to
|
||||||
|
# avoid needing privileged :22.
|
||||||
|
service:
|
||||||
|
ssh:
|
||||||
|
type: LoadBalancer
|
||||||
|
port: 2222
|
||||||
|
annotations:
|
||||||
|
lbipam.cilium.io/ips: "192.168.1.161"
|
||||||
|
|
||||||
gitea:
|
gitea:
|
||||||
admin:
|
admin:
|
||||||
existingSecret: forgejo-admin
|
existingSecret: forgejo-admin
|
||||||
@@ -22,8 +41,10 @@ gitea:
|
|||||||
server:
|
server:
|
||||||
DOMAIN: forgejo.riotpiao.com
|
DOMAIN: forgejo.riotpiao.com
|
||||||
ROOT_URL: https://forgejo.riotpiao.com
|
ROOT_URL: https://forgejo.riotpiao.com
|
||||||
SSH_DOMAIN: forgejo.riotpiao.com
|
# SSH clone URLs advertise git.riotpiao.com:2222 (the LoadBalancer above).
|
||||||
SSH_PORT: 22
|
SSH_DOMAIN: git.riotpiao.com
|
||||||
|
SSH_PORT: 2222
|
||||||
|
SSH_LISTEN_PORT: 2222
|
||||||
|
|
||||||
database:
|
database:
|
||||||
DB_TYPE: postgres
|
DB_TYPE: postgres
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ kind: Namespace
|
|||||||
metadata:
|
metadata:
|
||||||
name: cicd
|
name: cicd
|
||||||
labels:
|
labels:
|
||||||
# Baseline allows most workloads while blocking clearly dangerous configurations
|
# privileged: the forgejo-runner's dind (docker-in-docker) sidecar requires
|
||||||
# Redis needs some relaxed settings but doesn't need full privileged access
|
# securityContext.privileged=true, which baseline/restricted PSS reject
|
||||||
pod-security.kubernetes.io/enforce: baseline
|
# (the ReplicaSet silently creates 0 pods). gitea, redis and CNPG here are
|
||||||
pod-security.kubernetes.io/audit: baseline
|
# already privileged-tolerant.
|
||||||
pod-security.kubernetes.io/warn: baseline
|
pod-security.kubernetes.io/enforce: privileged
|
||||||
|
pod-security.kubernetes.io/audit: privileged
|
||||||
|
pod-security.kubernetes.io/warn: privileged
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
# ArgoCD CMP plugin for SOPS secret decryption
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: argocd-cmp-cm
|
|
||||||
namespace: argocd
|
|
||||||
data:
|
|
||||||
sops-secrets-v1.0.yaml: |
|
|
||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: ConfigManagementPlugin
|
|
||||||
metadata:
|
|
||||||
name: sops-secrets-v1.0
|
|
||||||
spec:
|
|
||||||
version: v1.0
|
|
||||||
init:
|
|
||||||
command: [sh, -c]
|
|
||||||
args:
|
|
||||||
- |
|
|
||||||
# Install sops if not present
|
|
||||||
if ! command -v sops &> /dev/null; then
|
|
||||||
wget -qO- https://github.com/getsops/sops/releases/download/v3.9.3/sops-v3.9.3.linux.amd64 > /usr/local/bin/sops
|
|
||||||
chmod +x /usr/local/bin/sops
|
|
||||||
fi
|
|
||||||
generate:
|
|
||||||
command: [sh, -c]
|
|
||||||
args:
|
|
||||||
- |
|
|
||||||
# Find all .enc.yaml files and decrypt them
|
|
||||||
find . -name '*.enc.yaml' -type f | while read -r file; do
|
|
||||||
sops -d "$file"
|
|
||||||
done
|
|
||||||
discover:
|
|
||||||
find:
|
|
||||||
glob: "**/*.enc.yaml"
|
|
||||||
@@ -6,23 +6,11 @@ global:
|
|||||||
|
|
||||||
# Server configuration
|
# Server configuration
|
||||||
server:
|
server:
|
||||||
|
# Ingress is managed declaratively in k8s/bootstrap/ingress/ingress.yaml
|
||||||
|
# (ssl-passthrough) instead of here — two Ingress objects for the same
|
||||||
|
# host caused undefined nginx routing behavior (502s). Do not re-enable.
|
||||||
ingress:
|
ingress:
|
||||||
enabled: true
|
enabled: false
|
||||||
ingressClassName: nginx
|
|
||||||
annotations:
|
|
||||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
|
||||||
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
|
|
||||||
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
|
|
||||||
hosts:
|
|
||||||
- argocd.riotpiao.com
|
|
||||||
tls:
|
|
||||||
- secretName: argocd-server-tls
|
|
||||||
hosts:
|
|
||||||
- argocd.riotpiao.com
|
|
||||||
|
|
||||||
# Allow insecure mode (terminate TLS at ingress)
|
|
||||||
extraArgs:
|
|
||||||
- --insecure
|
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
@@ -45,7 +33,7 @@ repoServer:
|
|||||||
cpu: 100m
|
cpu: 100m
|
||||||
memory: 256Mi
|
memory: 256Mi
|
||||||
limits:
|
limits:
|
||||||
cpu: 500m
|
cpu: 1000m
|
||||||
memory: 1Gi
|
memory: 1Gi
|
||||||
|
|
||||||
# Tolerations for control-plane
|
# Tolerations for control-plane
|
||||||
@@ -54,6 +42,52 @@ repoServer:
|
|||||||
operator: Exists
|
operator: Exists
|
||||||
effect: NoSchedule
|
effect: NoSchedule
|
||||||
|
|
||||||
|
# SOPS decryption is now done natively inside kustomize via the ksops exec
|
||||||
|
# generator (see k8s/argocd/secrets/), not a CMP sidecar. The repo-server's
|
||||||
|
# own `kustomize build --enable-alpha-plugins --enable-exec` runs ksops, which
|
||||||
|
# shells out to sops using the age key at SOPS_AGE_KEY_FILE. Install sops +
|
||||||
|
# ksops into a shared emptyDir on PATH; the repo-server container mounts them.
|
||||||
|
env:
|
||||||
|
- name: SOPS_AGE_KEY_FILE
|
||||||
|
value: /sops-age/key.txt
|
||||||
|
- name: XDG_CONFIG_HOME
|
||||||
|
value: /.config
|
||||||
|
initContainers:
|
||||||
|
- name: install-sops-ksops
|
||||||
|
image: alpine:3.20
|
||||||
|
command: [sh, -c]
|
||||||
|
args:
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
apk add --no-cache curl tar
|
||||||
|
curl -sSL -o /custom-tools/sops \
|
||||||
|
https://github.com/getsops/sops/releases/download/v3.9.3/sops-v3.9.3.linux.amd64
|
||||||
|
chmod +x /custom-tools/sops
|
||||||
|
curl -sSL https://github.com/viaduct-ai/kustomize-sops/releases/download/v4.5.1/ksops_4.5.1_Linux_x86_64.tar.gz \
|
||||||
|
| tar -xz -C /custom-tools ksops
|
||||||
|
chmod +x /custom-tools/ksops
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /custom-tools
|
||||||
|
name: custom-tools
|
||||||
|
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /usr/local/bin/sops
|
||||||
|
name: custom-tools
|
||||||
|
subPath: sops
|
||||||
|
- mountPath: /usr/local/bin/ksops
|
||||||
|
name: custom-tools
|
||||||
|
subPath: ksops
|
||||||
|
- mountPath: /sops-age
|
||||||
|
name: sops-age
|
||||||
|
readOnly: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: custom-tools
|
||||||
|
emptyDir: {}
|
||||||
|
- name: sops-age
|
||||||
|
secret:
|
||||||
|
secretName: sops-age
|
||||||
|
|
||||||
# Controller configuration
|
# Controller configuration
|
||||||
controller:
|
controller:
|
||||||
resources:
|
resources:
|
||||||
@@ -95,12 +129,41 @@ configs:
|
|||||||
cm:
|
cm:
|
||||||
admin.enabled: "true"
|
admin.enabled: "true"
|
||||||
application.instanceLabelKey: argocd.argoproj.io/instance
|
application.instanceLabelKey: argocd.argoproj.io/instance
|
||||||
|
# Let every kustomize build run the ksops exec generator.
|
||||||
|
kustomize.buildOptions: --enable-alpha-plugins --enable-exec
|
||||||
|
# External URL — required so OIDC redirect URIs are built correctly.
|
||||||
|
url: https://argocd.riotpiao.com
|
||||||
|
# Local accounts (in addition to Authentik SSO):
|
||||||
|
# rock — human admin; can log in with a password AND issue API tokens.
|
||||||
|
# cicd — automation-only; apiKey (token) for the CD pipeline, no UI login.
|
||||||
|
accounts.rock: apiKey,login
|
||||||
|
accounts.cicd: apiKey
|
||||||
|
# Authentik OIDC. clientSecret pulled from the argocd `oidc-secret` Secret
|
||||||
|
# (created by authentik-provision). The groups claim drives RBAC below.
|
||||||
|
oidc.config: |
|
||||||
|
name: Authentik
|
||||||
|
issuer: https://authentik.riotpiao.com/application/o/argocd/
|
||||||
|
clientID: argocd
|
||||||
|
clientSecret: $oidc-secret:client-secret
|
||||||
|
requestedScopes:
|
||||||
|
- openid
|
||||||
|
- profile
|
||||||
|
- email
|
||||||
|
- groups
|
||||||
|
requestedIDTokenClaims:
|
||||||
|
groups:
|
||||||
|
essential: true
|
||||||
|
|
||||||
params:
|
params:
|
||||||
server.insecure: true
|
server.insecure: false
|
||||||
|
|
||||||
# RBAC (allow admin full access)
|
# RBAC. local `admin` + `rock` + the `cicd` pipeline account all get role:admin;
|
||||||
|
# the Authentik `homelab-admins` group (rock is a member) maps to admin so SSO
|
||||||
|
# logins are admin too.
|
||||||
rbac:
|
rbac:
|
||||||
policy.default: role:readonly
|
policy.default: role:readonly
|
||||||
policy.csv: |
|
policy.csv: |
|
||||||
g, admin, role:admin
|
g, admin, role:admin
|
||||||
|
g, rock, role:admin
|
||||||
|
g, cicd, role:admin
|
||||||
|
g, homelab-admins, role:admin
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
# GitHub seed so ArgoCD can deploy everything after the control plane. After
|
# GitHub seed so ArgoCD can deploy everything after the control plane. After
|
||||||
# Forgejo is healthy + mirroring GitHub, cut over with phase5-cutover/root-app-forgejo.yaml.
|
# Forgejo is healthy + mirroring GitHub, cut over with phase5-cutover/root-app-forgejo.yaml.
|
||||||
#
|
#
|
||||||
# repoURL is the SSH form — must match the `url` in the seed-repo deploy-key Secret
|
# repoURL is anonymous HTTPS — the seed repo is public, so no deploy key and no
|
||||||
# (see seed-repo-secret.example.yaml). Apply that Secret before this.
|
# repository Secret are needed. Nothing to apply before this.
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Namespace
|
kind: Namespace
|
||||||
@@ -44,7 +44,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
project: homelab
|
project: homelab
|
||||||
source:
|
source:
|
||||||
repoURL: git@github.com:Riotpiaole/riotpiao.homelab.com.git # GitHub seed (SSH)
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git # GitHub seed (SSH)
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: k8s/argocd/apps
|
path: k8s/argocd/apps
|
||||||
destination:
|
destination:
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
# ArgoCD repo credential for the PRIVATE GitHub seed — deploy key (read-only).
|
|
||||||
# Apply at Phase 0 BEFORE root-app-github.yaml. This is a TEMPLATE: never commit
|
|
||||||
# the real private key.
|
|
||||||
#
|
|
||||||
# ssh-keygen -t ed25519 -C "argocd@homelab" -f argocd_seed -N ""
|
|
||||||
# # add argocd_seed.pub → GitHub repo → Settings → Deploy keys (Read-only, no write)
|
|
||||||
# kubectl create ns argocd --dry-run=client -o yaml | kubectl apply -f -
|
|
||||||
# kubectl -n argocd create secret generic seed-github-repo \
|
|
||||||
# --from-literal=type=git \
|
|
||||||
# [email protected]:Riotpiaole/riotpiao.homelab.com.git \
|
|
||||||
# --from-file=sshPrivateKey=argocd_seed
|
|
||||||
# kubectl -n argocd label secret seed-github-repo argocd.argoproj.io/secret-type=repository
|
|
||||||
#
|
|
||||||
# url MUST match root-app-github.yaml's repoURL (SSH form).
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Secret
|
|
||||||
metadata:
|
|
||||||
name: seed-github-repo
|
|
||||||
namespace: argocd
|
|
||||||
labels:
|
|
||||||
argocd.argoproj.io/secret-type: repository
|
|
||||||
stringData:
|
|
||||||
type: git
|
|
||||||
url: [email protected]:Riotpiaole/riotpiao.homelab.com.git
|
|
||||||
sshPrivateKey: |
|
|
||||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
|
||||||
REPLACE-WITH-READ-ONLY-DEPLOY-KEY-PRIVATE-HALF — DO NOT COMMIT THE REAL KEY
|
|
||||||
-----END OPENSSH PRIVATE KEY-----
|
|
||||||
@@ -104,5 +104,7 @@ spec:
|
|||||||
- name: docker-certs
|
- name: docker-certs
|
||||||
emptyDir: {} # DinD regenerates mTLS certs on each start
|
emptyDir: {} # DinD regenerates mTLS certs on each start
|
||||||
- name: homelab-ca
|
- name: homelab-ca
|
||||||
secret:
|
# homelab-ca is a ConfigMap (public CA trust bundle), not a Secret.
|
||||||
secretName: homelab-ca
|
# The volumeMounts use subPath: ca.crt to project the single cert file.
|
||||||
|
configMap:
|
||||||
|
name: homelab-ca
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
authentik:
|
|
||||||
secret_key: ENC[AES256_GCM,data:55ne/khf01ZD3FP2Zek+0Ar9C+GP/GPf33PwOwgXdnOXF4kLrEwwJ/E7yrH0nkBKGO221ClGBjlo7kb5vGMmmmg8TVBa1upQbk8b0s723n8=,iv:ZkDp4gOHY1eH44cDhGaZILi8dFDTNuYf1CVE+qrapzE=,tag:JTE4tfMJSowsn9YZ0xKFng==,type:str]
|
|
||||||
bootstrap_password: ENC[AES256_GCM,data:vBk/ivCG4x2TlvURrDxHAiaZlxrF+PeHBAuoZR6muWw=,iv:ccA703HMomR58cD2/6wK1W0IKJb7U1hNws6fmwG15E8=,tag:MdhTx9ebXLjRCnDpUxpJ0g==,type:str]
|
|
||||||
bootstrap_token: ENC[AES256_GCM,data:NwGZzaL8JufXYp6sjeeN1etdgaHT8IuaZFLZJegzs+fPXMvVCGPKKlStythIOa3gwgCXkhA+kV8+jyxYKr1UMw==,iv:EJCetXnkNe+UuhWk4f3vr3kIEbCuVplVKHPne3aVIQ8=,tag:kwlE+9O6DczO+mn9Rqx+Rw==,type:str]
|
|
||||||
postgresql_password: ENC[AES256_GCM,data:+WeZKo+24awwWfTAt5q6KJRkqaHD7YmlnKm4oGaUNvw=,iv:VSJ3d8p4f5SQM2lVdhSLY5dqdQTKwcfzOCATIM5M/cw=,tag:6K8mQmyLEg7KJi3bg+zPig==,type:str]
|
|
||||||
sops:
|
|
||||||
age:
|
|
||||||
- enc: |
|
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJSDJSYmRQNDdjWjlORmJX
|
|
||||||
Zm9yd2xRNi9XKy95Y2NQWUMyTGxjb1NjRTNzCnlNbko5NVJVSitiaE1DQ2QvMVoy
|
|
||||||
czRya01EaFZmZzAzcjRESG5NY1JIVTAKLS0tIFZ1UkREaGc4VUczMjFITWR6N0Ez
|
|
||||||
NStJZmpuWEw1dlRFcExZUlFLWkpBSFkK9r0NG3IKV7+AU00VXVCuHu+aBOOLydD0
|
|
||||||
ncioyDrJWgkyDxn9+BNZh4vX8vEERANYh+1/3P99Ubz28tifhmi2vg==
|
|
||||||
-----END AGE ENCRYPTED FILE-----
|
|
||||||
recipient: age1smu533f803gmd0jq60s2zaj9zlznajy0ca6rtewd4r37mr2hs3uqsrldfh
|
|
||||||
lastmodified: "2026-07-15T23:22:55Z"
|
|
||||||
mac: ENC[AES256_GCM,data:+MqbhlVnhriigDEj/AMJj4yLjcIjxkQc4QANcVeAXefcljHS4yOVtrwh2js3l9WVyIgVdl0MdlW+ycykBU4Pi3u4sdJoiJd945wZDsHvFCREYZIZdsVW/EHmKjIroqxQfeEtbWDStkaE+WTF/yGrZ2HLLztrXIafxZq7KhbXlo0=,iv:PCzhd8j539BxvpQQCds1vMXm3gfmlvSnrbtfOpFaKrY=,tag:NiHwSfQRvf//lpjFAKA1Jg==,type:str]
|
|
||||||
unencrypted_suffix: _unencrypted
|
|
||||||
version: 3.13.2
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user