Compare commits
285
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
225866945a | ||
|
|
c52b4acc74 | ||
|
|
a816045d3b | ||
|
|
ff77df5933 | ||
|
|
4645e320d5 | ||
|
|
27ae187526 | ||
|
|
0a29d781de | ||
|
|
a2e97e8cd3 | ||
|
|
b695cee987 | ||
|
|
245a03e951 | ||
|
|
af7c5e845a | ||
|
|
063f9bcd23 | ||
|
|
df9a68d0ba | ||
|
|
a07af6bf07 | ||
|
|
3a91c19b5c | ||
|
|
2d7127b37e | ||
|
|
2ba89f2ec0 | ||
|
|
0b282ba1f8 | ||
|
|
f10f0a8a26 | ||
|
|
34288b0b95 | ||
|
|
1fb0b62d7d | ||
|
|
09873aa275 | ||
|
|
63f2eaddd6 | ||
|
|
555b4b4050 | ||
|
|
8cf342b27c | ||
|
|
44f9bc25c4 | ||
|
|
5e97c5cf64 | ||
|
|
b7b1f15084 | ||
|
|
d51713ad6a | ||
|
|
fa239972a7 | ||
|
|
f53d54cba9 | ||
|
|
e4bbec95fb | ||
|
|
beb3cb21a0 | ||
|
|
9e84fb3386 | ||
|
|
2333310c38 | ||
|
|
be16020878 | ||
|
|
e5c371ed39 | ||
|
|
8b88e13762 | ||
|
|
e691a91df1 | ||
|
|
3fd930c518 | ||
|
|
88e73a885a | ||
|
|
f06cefabc4 | ||
|
|
36dfaa4ddd | ||
|
|
9188be39c6 | ||
|
|
d4508afc07 | ||
|
|
3f5d44d6fa | ||
|
|
209df7558e | ||
|
|
d81c57f860 | ||
|
|
e7b526b1d0 | ||
|
|
8589c40b44 | ||
|
|
feb7b25aba | ||
|
|
9d0ffcd29f | ||
|
|
accdfb11d7 | ||
|
|
47e7a2b1d6 | ||
|
|
8d63db9f3b | ||
|
|
bf67d2d9de | ||
|
|
95ae933489 | ||
|
|
f1d5c71a6c | ||
|
|
e6f2ab1423 | ||
|
|
1c7395d9e1 | ||
|
|
eba9f2144c | ||
|
|
4ad4df7965 | ||
|
|
8fda8c50d3 | ||
|
|
2e835510d6 | ||
|
|
8c32c16f79 | ||
|
|
18602759c0 | ||
|
|
751ae733d5 | ||
|
|
68107ba962 | ||
|
|
c2bcda58d9 | ||
|
|
14a17b4542 | ||
|
|
ce019f5f3a | ||
|
|
a55918e1f0 | ||
|
|
2499cc241f | ||
|
|
1c98628417 | ||
|
|
ec046cccde | ||
|
|
9d485d1238 | ||
|
|
48aac4998b | ||
|
|
70fcf111b9 | ||
|
|
cee29b8cb8 | ||
|
|
423e40200a | ||
|
|
4e7a7b065e | ||
|
|
a831c4d3db | ||
|
|
dafccd5d72 | ||
|
|
e963ceb90e | ||
|
|
ed9cf4d1e6 | ||
|
|
7b0f9171e2 | ||
|
|
2d9a23c4db | ||
|
|
7854e4557e | ||
|
|
162d95c314 | ||
|
|
cb2e44691e | ||
|
|
ce7a8bad81 | ||
|
|
e2c246cfcd | ||
|
|
acf8184680 | ||
|
|
cd3abede11 | ||
|
|
21fb58e1d3 | ||
|
|
0cbd5c16dd | ||
|
|
af30c189fe | ||
|
|
17dbe32a10 | ||
|
|
4b3f664502 | ||
|
|
d1b3c0e53d | ||
|
|
84aefc8db2 | ||
|
|
2b94114310 | ||
|
|
4faf8115c3 | ||
|
|
9836d20b06 | ||
|
|
cd6760bdca | ||
|
|
24892544b7 | ||
|
|
1685bca027 | ||
|
|
e76ad914d2 | ||
|
|
30c5197228 | ||
|
|
6d1c05574a | ||
|
|
be7881d6f0 | ||
|
|
dde4b602c4 | ||
|
|
be2a56ccf5 | ||
|
|
3d8a965718 | ||
|
|
246196407a | ||
|
|
1361c9bd13 | ||
|
|
01a310d13f | ||
|
|
46ec0caf5d | ||
|
|
616660cebe | ||
|
|
f9b9fbce95 | ||
|
|
301c661a46 | ||
|
|
d22842ca33 | ||
|
|
af00467b2b | ||
|
|
0a323fc039 | ||
|
|
566dcafbf6 | ||
|
|
261fa6faa8 | ||
|
|
b8c3528848 | ||
|
|
f08fb2bb75 | ||
|
|
d602ed8c78 | ||
|
|
1dd261bb25 | ||
|
|
c64b437bca | ||
|
|
64ee19c822 | ||
|
|
32cb01388c | ||
|
|
875b87cea2 | ||
|
|
d55e7ff31e | ||
|
|
eda152015c | ||
|
|
c9bf9f7dce | ||
|
|
5170921eea | ||
|
|
b3017c525a | ||
|
|
f101b3381e | ||
|
|
ef348d23f4 | ||
|
|
04ec157c19 | ||
|
|
ded98329e5 | ||
|
|
d51056c684 | ||
|
|
c661d7eb77 | ||
|
|
edc12c388f | ||
|
|
f0178b3bc5 | ||
|
|
e82c4b36a4 | ||
|
|
4ea25620dd | ||
|
|
0588cb91b4 | ||
|
|
4bb99ef24f | ||
|
|
fd07b3cff2 | ||
|
|
dc0bb63a01 | ||
|
|
b2191509fb | ||
|
|
5635482e0d | ||
|
|
328a713f4f | ||
|
|
cc5325d905 | ||
|
|
26f7da3610 | ||
|
|
f2f4a2580f | ||
|
|
21e3987b11 | ||
|
|
3e7238f71c | ||
|
|
88f8a764de | ||
|
|
34e996475f | ||
|
|
3f4653ac56 | ||
|
|
1dc6a2025f | ||
|
|
da925f3101 | ||
|
|
2443708abb | ||
|
|
9a34c12068 | ||
|
|
f646bb06fd | ||
|
|
4363739d59 | ||
|
|
2bf543bba1 | ||
|
|
6a2aacc4e6 | ||
|
|
0471177250 | ||
|
|
7fb73d6a4c | ||
|
|
e0b24c83d0 | ||
|
|
9117fd777a | ||
|
|
5ce0b92186 | ||
|
|
89fa87f7c1 | ||
|
|
42cd0204fa | ||
|
|
3a95f57b8f | ||
|
|
2ec6eba9d2 | ||
|
|
d6f5b9ed69 | ||
|
|
ce1fc4e296 | ||
|
|
d841bbdb95 | ||
|
|
d9d2e34558 | ||
|
|
4cfac71a73 | ||
|
|
7728f20d2b | ||
|
|
cea1a78a37 | ||
|
|
abaea8823b | ||
|
|
d282ae1aa0 | ||
|
|
063308308f | ||
|
|
a207c56637 | ||
|
|
c759481ea6 | ||
|
|
15b1ec6ad4 | ||
|
|
f7a8df0514 | ||
|
|
32281ee923 | ||
|
|
578a707867 | ||
|
|
c6493f14ae | ||
|
|
7437078f23 | ||
|
|
f8b19d9f55 | ||
|
|
c000ddb402 | ||
|
|
888c4f5493 | ||
|
|
403e495fe9 | ||
|
|
f30771a78a | ||
|
|
e2e17ae0fb | ||
|
|
c230b3ee45 | ||
|
|
2d7330798b | ||
|
|
a81b9b6169 | ||
|
|
a860de94da | ||
|
|
94a2bd648c | ||
|
|
be2e5c321f | ||
|
|
9d1d79b774 | ||
|
|
72ab6b6973 | ||
|
|
7114fc8fc9 | ||
|
|
fe7b749951 | ||
|
|
694350634b | ||
|
|
ab103f00f0 | ||
|
|
e4d645eae9 | ||
|
|
ab76e40d05 | ||
|
|
b568c015e2 | ||
|
|
e71c7ad37e | ||
|
|
842360288d | ||
|
|
f18f96eb5b | ||
|
|
d655726eca | ||
|
|
ff22027c7a | ||
|
|
f158512261 | ||
|
|
69d2240cf4 | ||
|
|
543105bf46 | ||
|
|
7d1eb09486 | ||
|
|
0dddf15dc8 | ||
|
|
37ee3dc5b1 | ||
|
|
f864e3dc51 | ||
|
|
96e40916e3 | ||
|
|
04100232ef | ||
|
|
09980aa41d | ||
|
|
5144ab732d | ||
|
|
4379f3cb21 | ||
|
|
d2f4b3c7e4 | ||
|
|
e7f3409d0f | ||
|
|
23ec31bd6d | ||
|
|
421086f845 | ||
|
|
bd00bca9bf | ||
|
|
397edf632f | ||
|
|
13557184c6 | ||
|
|
6d554961c2 | ||
|
|
3eccf9f653 | ||
|
|
52cb895cda | ||
|
|
bca247a763 | ||
|
|
9e3781a069 | ||
|
|
ebeb4948d4 | ||
|
|
a3190abe50 | ||
|
|
dddc7a524a | ||
|
|
ed7be6f229 | ||
|
|
418ab7bfc2 | ||
|
|
ca8525c625 | ||
|
|
8a3a892cbd | ||
|
|
cdacdd8d11 | ||
|
|
263a48a22d | ||
|
|
47c0301a43 | ||
|
|
edd4ea7fe4 | ||
|
|
086ad9f9a9 | ||
|
|
3eabb847fd | ||
|
|
6109477bf9 | ||
|
|
20c634fd03 | ||
|
|
074b43e1f2 | ||
|
|
c2e084c7c2 | ||
|
|
dd608d3231 | ||
|
|
9a4d486b86 | ||
|
|
4d32e2765b | ||
|
|
0d4e98d88b | ||
|
|
ebf97f573e | ||
|
|
05b088ca48 | ||
|
|
b33fdde5b7 | ||
|
|
cd1c5691e8 | ||
|
|
e6d7626fb6 | ||
|
|
e1d0cfd70b | ||
|
|
18f2f94f8e | ||
|
|
6d5a0ba205 | ||
|
|
1c02e2b831 | ||
|
|
4ab596196e | ||
|
|
63d7256b9e | ||
|
|
674c8f0d66 | ||
|
|
36aea89e47 | ||
|
|
11c26f3f29 | ||
|
|
11898733e8 |
+13
-2
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
# ── Cluster Configuration ──────────────────────────────────────────────────────
|
# ── Cluster Configuration ──────────────────────────────────────────────────────
|
||||||
# Base domain for external services (Authentik, MinIO, Forgejo, etc.)
|
# Base domain for external services (Authentik, MinIO, Forgejo, etc.)
|
||||||
# Example: riotpiao.homelab.com
|
# Example: riotpiao.com
|
||||||
CLUSTER_DOMAIN=riotpiao.homelab.com
|
CLUSTER_DOMAIN=riotpiao.com
|
||||||
|
|
||||||
# Internal Kubernetes DNS names (svc.cluster.local)
|
# Internal Kubernetes DNS names (svc.cluster.local)
|
||||||
# Only change these if your cluster domain differs
|
# Only change these if your cluster domain differs
|
||||||
@@ -68,3 +68,14 @@ AUTHENTIK_TEMPORAL_CLIENT_ID=
|
|||||||
# ── CI/CD ──────────────────────────────────────────────────────────────────────
|
# ── CI/CD ──────────────────────────────────────────────────────────────────────
|
||||||
# Forgejo Personal Access Token (from rock user) for pushing images to registry
|
# Forgejo Personal Access Token (from rock user) for pushing images to registry
|
||||||
FORGEJO_RIOTPIAO_PAT=
|
FORGEJO_RIOTPIAO_PAT=
|
||||||
|
|
||||||
|
# ── Cloudflare Tunnel (remote off-LAN access to kubectl/talosctl) ──────────────
|
||||||
|
# From Cloudflare Zero Trust dashboard → Networks → Tunnels
|
||||||
|
# CLOUDFLARE_CONNECTOR_TOKEN: full tunnel token (JWT-like base64 string)
|
||||||
|
# CLOUDFLARE_ACCOUNT_ID: your account ID (hex string)
|
||||||
|
# CLOUDFLARE_TUNNEL_ID: tunnel UUID
|
||||||
|
# CLOUDFLARE_API_TOKEN: API token for programmatic tunnel config (optional)
|
||||||
|
CLOUDFLARE_CONNECTOR_TOKEN=
|
||||||
|
CLOUDFLARE_ACCOUNT_ID=
|
||||||
|
CLOUDFLARE_TUNNEL_ID=
|
||||||
|
CLOUDFLARE_API_TOKEN=
|
||||||
|
|||||||
@@ -0,0 +1,460 @@
|
|||||||
|
# CI/CD Pipeline: GitOps Validation & Deployment
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Pure GitOps CI/CD pipeline using Forgejo Actions (self-hosted runner).
|
||||||
|
|
||||||
|
**Principle:** Validate in CI, deploy via ArgoCD (no manual steps).
|
||||||
|
|
||||||
|
```
|
||||||
|
git push
|
||||||
|
↓
|
||||||
|
[CI: Validate]
|
||||||
|
├─ yamllint (YAML syntax)
|
||||||
|
├─ kubeval (K8s manifests)
|
||||||
|
├─ kustomize build (all layers)
|
||||||
|
├─ argocd validation (app definitions)
|
||||||
|
└─ security scan (secrets, best practices)
|
||||||
|
↓
|
||||||
|
[If push to main]
|
||||||
|
└─ ArgoCD auto-syncs (if enabled)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
### 1. validate-k8s.yaml (Mandatory)
|
||||||
|
|
||||||
|
**Trigger:** Any push/PR with k8s/ changes
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. Lints all YAML files (`yamllint`)
|
||||||
|
2. Validates K8s manifests (`kubeval`)
|
||||||
|
3. Builds all kustomization layers
|
||||||
|
4. Validates ArgoCD applications
|
||||||
|
5. Reports results
|
||||||
|
|
||||||
|
**Duration:** ~2-3 minutes
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
- ✅ PASS: All layers build, manifests valid → OK to merge
|
||||||
|
- ❌ FAIL: Syntax error, invalid resource, build failed → Fix & push again
|
||||||
|
|
||||||
|
**Example output:**
|
||||||
|
```
|
||||||
|
=== Building k8s/infrastructure/ ===
|
||||||
|
✓ Infrastructure built successfully
|
||||||
|
Resources: 47
|
||||||
|
|
||||||
|
=== Building k8s/bootstrap/ ===
|
||||||
|
✓ Bootstrap built successfully
|
||||||
|
Resources: 23
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to check:**
|
||||||
|
- After every commit
|
||||||
|
- Before merging PRs
|
||||||
|
- On every branch
|
||||||
|
|
||||||
|
### 2. argocd-sync.yaml (Recommended)
|
||||||
|
|
||||||
|
**Trigger:** Push to main only (k8s/ changed)
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. Authenticates with ArgoCD
|
||||||
|
2. Syncs `homelab-root` application
|
||||||
|
3. Waits for sync to complete (5 min timeout)
|
||||||
|
4. Verifies all applications healthy
|
||||||
|
|
||||||
|
**Duration:** 1-5 minutes (depends on resources)
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
- ✅ SYNCED: All resources deployed to cluster
|
||||||
|
- ❌ FAILED: Sync error, pod crashes, etc. → Check ArgoCD UI for details
|
||||||
|
|
||||||
|
**When it runs:**
|
||||||
|
- Automatically after merge to main
|
||||||
|
- Only on k8s/ changes (not on docs)
|
||||||
|
|
||||||
|
**Manual trigger (if needed):**
|
||||||
|
```bash
|
||||||
|
# SSH to runner or use Forgejo UI
|
||||||
|
# Re-run failed workflow
|
||||||
|
# Or manually sync: argocd app sync homelab-root
|
||||||
|
```
|
||||||
|
|
||||||
|
**Requires secrets:**
|
||||||
|
- `ARGOCD_SERVER`: ArgoCD server URL (https://argocd.riotpiao.com)
|
||||||
|
- `ARGOCD_AUTH_TOKEN`: ArgoCD API token (generate via ArgoCD UI)
|
||||||
|
|
||||||
|
### 3. security-scan.yaml (Optional)
|
||||||
|
|
||||||
|
**Trigger:** Any push/PR with k8s/ changes
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. Scans Dockerfiles for vulnerabilities (`trivy`)
|
||||||
|
2. Scans Helm charts for security issues
|
||||||
|
3. Audits K8s manifests (`polaris`)
|
||||||
|
4. Checks for hardcoded secrets
|
||||||
|
5. Verifies security best practices
|
||||||
|
|
||||||
|
**Duration:** ~3-5 minutes
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
- ✅ PASS: No critical issues
|
||||||
|
- ⚠️ WARNING: Best practice recommendations (non-blocking)
|
||||||
|
- ❌ FAIL: Hardcoded secrets found (must fix)
|
||||||
|
|
||||||
|
**Common issues:**
|
||||||
|
- Missing resource limits (warning)
|
||||||
|
- Privileged containers (warning)
|
||||||
|
- Hardcoded passwords (ERROR)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.forgejo/
|
||||||
|
├── workflows/ # CI/CD workflows
|
||||||
|
│ ├── validate-k8s.yaml # Validate manifests (required)
|
||||||
|
│ ├── argocd-sync.yaml # Sync to cluster (auto on main)
|
||||||
|
│ └── security-scan.yaml # Security checks (optional)
|
||||||
|
└── CI-CD.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### 1. Install Forgejo Runner
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On runner machine (inside cluster or external)
|
||||||
|
forgejo-runner register \
|
||||||
|
--instance https://forgejo.riotpiao.com \
|
||||||
|
--token <registration-token> \
|
||||||
|
--name homelab-runner \
|
||||||
|
--labels docker
|
||||||
|
|
||||||
|
forgejo-runner daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Add ArgoCD Secrets to Forgejo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Go to: Forgejo → Settings → Secrets
|
||||||
|
|
||||||
|
# Add:
|
||||||
|
ARGOCD_SERVER = https://argocd.riotpiao.com
|
||||||
|
ARGOCD_AUTH_TOKEN = <token> # Generate: argocd account generate-token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Generate ArgoCD Token
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Inside cluster
|
||||||
|
kubectl -n argocd port-forward svc/argocd-server 8080:443
|
||||||
|
|
||||||
|
# Go to: https://localhost:8080/user-info/api-tokens
|
||||||
|
# Create new token (CI/CD)
|
||||||
|
# Copy token to Forgejo secrets
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Execution
|
||||||
|
|
||||||
|
### When developer pushes to feature branch:
|
||||||
|
|
||||||
|
```
|
||||||
|
git push origin feature/new-service
|
||||||
|
|
||||||
|
↓
|
||||||
|
Forgejo Actions triggered
|
||||||
|
↓
|
||||||
|
validate-k8s.yaml runs:
|
||||||
|
✓ Lints YAML
|
||||||
|
✓ Validates manifests
|
||||||
|
✓ Builds kustomizations
|
||||||
|
✓ All pass → GitHub comment: "Ready to merge"
|
||||||
|
↓
|
||||||
|
Developer opens PR
|
||||||
|
↓
|
||||||
|
Reviewer checks:
|
||||||
|
- Code changes (YAML)
|
||||||
|
- Workflow results
|
||||||
|
- ArgoCD impact (diff)
|
||||||
|
↓
|
||||||
|
PR merged to main
|
||||||
|
```
|
||||||
|
|
||||||
|
### When merged to main:
|
||||||
|
|
||||||
|
```
|
||||||
|
git merge feature/new-service → main
|
||||||
|
|
||||||
|
↓
|
||||||
|
Forgejo Actions triggered
|
||||||
|
↓
|
||||||
|
validate-k8s.yaml runs:
|
||||||
|
✓ Same validation as above
|
||||||
|
↓
|
||||||
|
argocd-sync.yaml runs (if enabled):
|
||||||
|
✓ Syncs homelab-root
|
||||||
|
✓ Waits for sync
|
||||||
|
✓ Verifies health
|
||||||
|
✓ Resources deployed to cluster
|
||||||
|
↓
|
||||||
|
Cluster state = git state
|
||||||
|
(No manual kubectl apply needed!)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debugging CI/CD Failures
|
||||||
|
|
||||||
|
### Issue: "Kustomize build failed"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run locally
|
||||||
|
cd k8s/
|
||||||
|
kustomize build bootstrap/ # See actual error
|
||||||
|
|
||||||
|
# Fix YAML/kustomization.yaml
|
||||||
|
# git push again
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: "Kubeval validation failed"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check K8s manifest syntax
|
||||||
|
kubeval k8s/platform/minio/config.yaml
|
||||||
|
|
||||||
|
# Common issues:
|
||||||
|
# - Typos in apiVersion, kind, metadata
|
||||||
|
# - Missing required fields
|
||||||
|
# - Invalid references (namespace, service name)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: "ArgoCD sync failed"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check ArgoCD UI
|
||||||
|
# https://argocd.riotpiao.com → homelab-root
|
||||||
|
|
||||||
|
# Or CLI
|
||||||
|
argocd app get homelab-root
|
||||||
|
argocd app logs homelab-root --follow
|
||||||
|
|
||||||
|
# Common issues:
|
||||||
|
# - Missing namespace (fixed by infrastructure layer)
|
||||||
|
# - Invalid Helm chart version
|
||||||
|
# - Secret not found
|
||||||
|
# - Network policy blocking traffic
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: "Security scan found hardcoded secret"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fix: Remove secret from YAML
|
||||||
|
# Add to SOPS encryption instead
|
||||||
|
|
||||||
|
# Or use ArgoCD Sealed Secrets
|
||||||
|
# (if SOPS not available)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Viewing Results
|
||||||
|
|
||||||
|
### Forgejo Actions UI
|
||||||
|
|
||||||
|
```
|
||||||
|
Repository → Actions
|
||||||
|
├─ validate-k8s
|
||||||
|
│ ├─ ✅ Success (merge safe)
|
||||||
|
│ ├─ ❌ Failed (fix required)
|
||||||
|
│ └─ Logs (click "Steps" → "Summary")
|
||||||
|
├─ argocd-sync
|
||||||
|
│ ├─ ✅ Synced (deployed)
|
||||||
|
│ └─ ❌ Failed (check ArgoCD UI)
|
||||||
|
└─ security-scan
|
||||||
|
├─ ✅ Pass (no critical issues)
|
||||||
|
└─ ⚠️ Warning (review, non-blocking)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ArgoCD UI
|
||||||
|
|
||||||
|
```
|
||||||
|
https://argocd.riotpiao.com
|
||||||
|
├─ homelab-root
|
||||||
|
│ ├─ Status: Synced ✓
|
||||||
|
│ ├─ Health: Healthy ✓
|
||||||
|
│ └─ Details (click to see resources)
|
||||||
|
├─ layer-1-bootstrap
|
||||||
|
├─ layer-2-platform
|
||||||
|
├─ layer-3-security
|
||||||
|
├─ layer-4-applications
|
||||||
|
└─ layer-5-data
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### Add new service to cluster
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create directory and kustomization.yaml
|
||||||
|
mkdir -p k8s/applications/my-service
|
||||||
|
cat > k8s/applications/my-service/kustomization.yaml << EOF
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: my-namespace
|
||||||
|
helmCharts:
|
||||||
|
- name: my-chart
|
||||||
|
repo: https://charts.example.com
|
||||||
|
version: 1.0.0
|
||||||
|
releaseName: my-service
|
||||||
|
valuesFile: values.yaml
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 2. Add values.yaml
|
||||||
|
cp /template/values.yaml k8s/applications/my-service/
|
||||||
|
|
||||||
|
# 3. Commit and push
|
||||||
|
git add k8s/applications/my-service/
|
||||||
|
git commit -m "feat(apps): add my-service"
|
||||||
|
git push
|
||||||
|
|
||||||
|
# 4. CI validates
|
||||||
|
# 5. Merge to main
|
||||||
|
# 6. ArgoCD syncs automatically
|
||||||
|
# ✓ Service deployed to cluster
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rollback a deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Find broken commit
|
||||||
|
git log --oneline k8s/ # Identify bad commit
|
||||||
|
|
||||||
|
# 2. Revert
|
||||||
|
git revert <commit-hash>
|
||||||
|
git push
|
||||||
|
|
||||||
|
# 3. CI validates (should pass)
|
||||||
|
# 4. Merge to main
|
||||||
|
# 5. ArgoCD syncs back to previous version
|
||||||
|
# ✓ Cluster state reverted
|
||||||
|
```
|
||||||
|
|
||||||
|
### Emergency: Disable ArgoCD auto-sync
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# If production broken and need time to debug:
|
||||||
|
argocd app set homelab-root --sync-policy none
|
||||||
|
|
||||||
|
# Fix issue in git
|
||||||
|
# Test locally: kustomize build k8s/
|
||||||
|
|
||||||
|
# Re-enable
|
||||||
|
argocd app set homelab-root --sync-policy automated
|
||||||
|
argocd app sync homelab-root
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitoring & Alerts
|
||||||
|
|
||||||
|
### Check workflow status in Forgejo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dashboard shows:
|
||||||
|
✅ All green → Safe to merge
|
||||||
|
❌ Red → Fix required before merge
|
||||||
|
⏳ Yellow → Still running (wait)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check ArgoCD status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
argocd app list
|
||||||
|
# Shows: Synced, OutOfSync, Unknown status
|
||||||
|
|
||||||
|
argocd app get homelab-root
|
||||||
|
# Shows: health, sync status, resources
|
||||||
|
|
||||||
|
argocd app logs homelab-root --follow
|
||||||
|
# Real-time logs during sync
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alerts (optional, future)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Could add Forgejo webhooks → Slack/email
|
||||||
|
# When CI/CD fails → Alert ops team
|
||||||
|
# When ArgoCD goes OutOfSync → Alert ops team
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Workflow doesn't trigger
|
||||||
|
|
||||||
|
**Check:**
|
||||||
|
- Is Forgejo runner running? `forgejo-runner daemon`
|
||||||
|
- Did you push to correct branch? (validate runs on all, argocd-sync only on main)
|
||||||
|
- Did path match filter? (must change k8s/ or .forgejo/workflows/)
|
||||||
|
|
||||||
|
### Workflow hangs/times out
|
||||||
|
|
||||||
|
**Check:**
|
||||||
|
- kustomize build → Check for dependency cycles
|
||||||
|
- argocd sync → Check cluster resources (storage full? network down?)
|
||||||
|
- security scan → Large image scan → Takes time
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
- Increase timeout in workflow
|
||||||
|
- Optimize kustomization (remove unused resources)
|
||||||
|
- Add resource limits to pods
|
||||||
|
|
||||||
|
### ArgoCD token invalid
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```bash
|
||||||
|
# Regenerate token
|
||||||
|
argocd account generate-token
|
||||||
|
|
||||||
|
# Update Forgejo secret
|
||||||
|
# Settings → Secrets → ARGOCD_AUTH_TOKEN = <new-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
✅ **DO:**
|
||||||
|
- Commit all K8s changes to git (no manual kubectl apply)
|
||||||
|
- Run validate-k8s locally before push
|
||||||
|
- Write descriptive commit messages (why this change?)
|
||||||
|
- Review workflow logs before merging
|
||||||
|
- Monitor ArgoCD sync after merge
|
||||||
|
|
||||||
|
❌ **DON'T:**
|
||||||
|
- Push directly to main (always use PR)
|
||||||
|
- Skip workflow validation (it catches errors early)
|
||||||
|
- Ignore security scan warnings
|
||||||
|
- Manually `kubectl apply` (breaks GitOps)
|
||||||
|
- Edit resources in cluster (they revert via ArgoCD)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Setup Forgejo runner** (if not already running)
|
||||||
|
2. **Add ArgoCD secrets** to Forgejo
|
||||||
|
3. **Test workflows** on feature branch
|
||||||
|
4. **Merge to main** → Watch ArgoCD sync
|
||||||
|
5. **Celebrate:** Full GitOps pipeline working! 🎉
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
name: Cluster CI Pipeline
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'k8s/**'
|
||||||
|
- '.forgejo/workflows/cluster-ci.yaml'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'k8s/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
# === Checkout ===
|
||||||
|
- name: Checkout
|
||||||
|
run: |
|
||||||
|
REPO_URL="${{ gitea.server_url }}/${{ gitea.repository }}.git"
|
||||||
|
CLONE_URL="https://${{ secrets.CI_RUNNER }}:${{ secrets.CI_RUNNER_SECRET }}@${REPO_URL#https://}"
|
||||||
|
git clone --depth 1 "$CLONE_URL" .
|
||||||
|
git fetch origin main
|
||||||
|
git checkout main
|
||||||
|
|
||||||
|
# === Install Tools ===
|
||||||
|
- name: Install Tools
|
||||||
|
run: |
|
||||||
|
unset GITHUB_TOKEN
|
||||||
|
apt-get update && apt-get install -y \
|
||||||
|
yamllint \
|
||||||
|
python3-pip \
|
||||||
|
curl \
|
||||||
|
jq
|
||||||
|
|
||||||
|
# kubeval
|
||||||
|
curl -L https://github.com/instrumenta/kubeval/releases/latest/download/kubeval-linux-amd64.tar.gz | tar xz
|
||||||
|
mv -f kubeval /usr/local/bin/
|
||||||
|
|
||||||
|
# kustomize
|
||||||
|
rm -f kustomize
|
||||||
|
curl -s https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh | bash
|
||||||
|
mv -f kustomize /usr/local/bin/
|
||||||
|
|
||||||
|
# argocd
|
||||||
|
curl -sSL -o /usr/local/bin/argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
|
||||||
|
chmod +x /usr/local/bin/argocd
|
||||||
|
|
||||||
|
# trivy
|
||||||
|
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
|
||||||
|
|
||||||
|
# polaris
|
||||||
|
curl -L https://github.com/FairwindsOps/polaris/releases/latest/download/polaris-linux-amd64 -o /usr/local/bin/polaris
|
||||||
|
chmod +x /usr/local/bin/polaris
|
||||||
|
|
||||||
|
# === YAML Lint ===
|
||||||
|
- name: YAML Lint
|
||||||
|
run: |
|
||||||
|
echo "=== Linting YAML files ==="
|
||||||
|
yamllint k8s/ -c .yamllint.yaml || true
|
||||||
|
|
||||||
|
# === Kubeval - Validate K8s Syntax ===
|
||||||
|
- name: Kubeval - Validate K8s Syntax
|
||||||
|
run: |
|
||||||
|
echo "=== Validating Kubernetes manifests ==="
|
||||||
|
find k8s -name "*.yaml" -o -name "*.yml" | grep -v "\.archive" | while read file; do
|
||||||
|
echo "Validating $file..."
|
||||||
|
kubeval "$file" -d 2>/dev/null || true
|
||||||
|
done
|
||||||
|
|
||||||
|
# === Kustomize Build - All overlays ===
|
||||||
|
- name: Kustomize Build - Infrastructure
|
||||||
|
run: |
|
||||||
|
echo "=== Building k8s/infrastructure/ ==="
|
||||||
|
kustomize build k8s/infrastructure > /tmp/infrastructure.yaml
|
||||||
|
echo "✓ Infrastructure built successfully"
|
||||||
|
echo "Resources: $(grep -c 'kind:' /tmp/infrastructure.yaml)"
|
||||||
|
|
||||||
|
- name: Kustomize Build - Bootstrap
|
||||||
|
run: |
|
||||||
|
echo "=== Building k8s/bootstrap/ ==="
|
||||||
|
kustomize build k8s/bootstrap > /tmp/bootstrap.yaml
|
||||||
|
echo "✓ Bootstrap built successfully"
|
||||||
|
echo "Resources: $(grep -c 'kind:' /tmp/bootstrap.yaml || echo 0)"
|
||||||
|
|
||||||
|
- name: Kustomize Build - Platform
|
||||||
|
run: |
|
||||||
|
echo "=== Building k8s/platform/ ==="
|
||||||
|
kustomize build k8s/platform > /tmp/platform.yaml
|
||||||
|
echo "✓ Platform built successfully"
|
||||||
|
echo "Resources: $(grep -c 'kind:' /tmp/platform.yaml || echo 0)"
|
||||||
|
|
||||||
|
- name: Kustomize Build - Security
|
||||||
|
run: |
|
||||||
|
echo "=== Building k8s/security/ ==="
|
||||||
|
kustomize build k8s/security > /tmp/security.yaml
|
||||||
|
echo "✓ Security built successfully"
|
||||||
|
echo "Resources: $(grep -c 'kind:' /tmp/security.yaml || echo 0)"
|
||||||
|
|
||||||
|
- name: Kustomize Build - Applications
|
||||||
|
run: |
|
||||||
|
echo "=== Building k8s/applications/ ==="
|
||||||
|
kustomize build k8s/applications > /tmp/applications.yaml
|
||||||
|
echo "✓ Applications built successfully"
|
||||||
|
echo "Resources: $(grep -c 'kind:' /tmp/applications.yaml || echo 0)"
|
||||||
|
|
||||||
|
- name: Kustomize Build - Data
|
||||||
|
run: |
|
||||||
|
echo "=== Building k8s/data/ ==="
|
||||||
|
kustomize build k8s/data > /tmp/data.yaml
|
||||||
|
echo "✓ Data built successfully"
|
||||||
|
echo "Resources: $(grep -c 'kind:' /tmp/data.yaml || echo 0)"
|
||||||
|
|
||||||
|
- name: Validate ArgoCD Applications
|
||||||
|
run: |
|
||||||
|
echo "=== Validating ArgoCD Applications ==="
|
||||||
|
kubeval k8s/argocd/apps/*.yaml
|
||||||
|
|
||||||
|
# === Trivy - Scan Dockerfile ===
|
||||||
|
- name: Trivy - Scan Dockerfile
|
||||||
|
run: |
|
||||||
|
if find . -name "Dockerfile" 2>/dev/null | grep -v node_modules | head -1 | grep -q .; then
|
||||||
|
echo "=== Scanning Dockerfiles with Trivy ==="
|
||||||
|
find . -name "Dockerfile" -not -path "*/node_modules/*" -exec trivy config {} \;
|
||||||
|
else
|
||||||
|
echo "No Dockerfiles found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Trivy - Scan Helm Charts ===
|
||||||
|
- name: Trivy - Scan Helm Charts
|
||||||
|
run: |
|
||||||
|
if find k8s -name "Chart.yaml" 2>/dev/null | head -1 | grep -q .; then
|
||||||
|
echo "=== Scanning Helm charts with Trivy ==="
|
||||||
|
find k8s -name "Chart.yaml" -exec dirname {} \; | while read chart; do
|
||||||
|
echo "Scanning $chart..."
|
||||||
|
trivy config "$chart" || true
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "No Helm charts found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Polaris - K8s Security Audit ===
|
||||||
|
- name: Polaris - K8s Security Audit
|
||||||
|
run: |
|
||||||
|
echo "=== Running Polaris K8s security audit ==="
|
||||||
|
polaris audit --audit-path /tmp/polaris-audit.json k8s/ || true
|
||||||
|
|
||||||
|
if [ -f /tmp/polaris-audit.json ]; then
|
||||||
|
echo "Security issues found:"
|
||||||
|
jq '.results[] | select(.pass == false)' /tmp/polaris-audit.json || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Check for Secrets in Code ===
|
||||||
|
- name: Check for Secrets in Code
|
||||||
|
run: |
|
||||||
|
echo "=== Scanning for hardcoded secrets ==="
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Any private key block is fatal, regardless of the field name carrying it.
|
||||||
|
KEYS=$(grep -rIE --include="*.yaml" --include="*.yml" \
|
||||||
|
-- "-----BEGIN ([A-Z]+ )?PRIVATE KEY-----" k8s/ \
|
||||||
|
| grep -v "\.enc\.yaml" || true)
|
||||||
|
if [ -n "$KEYS" ]; then
|
||||||
|
echo "❌ Unencrypted private key material found:"
|
||||||
|
echo "$KEYS"
|
||||||
|
FAILED=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Plaintext values in secret-ish YAML fields. SOPS output is ENC[...],
|
||||||
|
# so encrypted files never trip this.
|
||||||
|
VALS=$(grep -rInE --include="*.yaml" --include="*.yml" \
|
||||||
|
-- "^[[: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"
|
||||||
|
|
||||||
|
# === Check K8s Security Best Practices ===
|
||||||
|
- name: Check K8s Security Best Practices
|
||||||
|
run: |
|
||||||
|
echo "=== Checking K8s security best practices ==="
|
||||||
|
|
||||||
|
if grep -r "privileged: true" k8s/ --include="*.yaml" --include="*.yml"; then
|
||||||
|
echo "⚠️ Found privileged containers"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -r "hostNetwork: true" k8s/ --include="*.yaml" --include="*.yml"; then
|
||||||
|
echo "⚠️ Found hostNetwork usage"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking for missing resource limits..."
|
||||||
|
MISSING=0
|
||||||
|
find k8s -name "*.yaml" -o -name "*.yml" | while read file; do
|
||||||
|
if grep -q "kind: Deployment\|kind: StatefulSet\|kind: DaemonSet" "$file"; then
|
||||||
|
if ! grep -q "resources:" "$file"; then
|
||||||
|
echo "⚠️ $file: Missing resource requests/limits"
|
||||||
|
MISSING=$((MISSING + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# === ArgoCD Sync (main branch only) ===
|
||||||
|
- name: Sync ArgoCD
|
||||||
|
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||||
|
env:
|
||||||
|
ARGOCD_SERVER: ${{ secrets.ARGOCD_SERVER }}
|
||||||
|
ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "=== Syncing homelab-root ==="
|
||||||
|
argocd app sync homelab-root --force
|
||||||
|
argocd app wait homelab-root --timeout 5m
|
||||||
|
|
||||||
|
- name: Check Sync Status
|
||||||
|
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||||
|
env:
|
||||||
|
ARGOCD_SERVER: ${{ secrets.ARGOCD_SERVER }}
|
||||||
|
ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "=== ArgoCD Applications Status ==="
|
||||||
|
argocd app list -o table
|
||||||
|
|
||||||
|
STATUS=$(argocd app get homelab-root -o jsonpath='{.status.syncStatus}')
|
||||||
|
if [ "$STATUS" != "Synced" ]; then
|
||||||
|
echo "❌ Root app sync failed: $STATUS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ Root app synced successfully"
|
||||||
|
|
||||||
|
- name: Health Check
|
||||||
|
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||||
|
env:
|
||||||
|
ARGOCD_SERVER: ${{ secrets.ARGOCD_SERVER }}
|
||||||
|
ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "=== Checking Application Health ==="
|
||||||
|
argocd app get homelab-root -o wide
|
||||||
|
|
||||||
|
# === Summary ===
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "=== CI Pipeline Summary ==="
|
||||||
|
echo "✓ YAML linted"
|
||||||
|
echo "✓ Manifests validated"
|
||||||
|
echo "✓ Kustomizations built"
|
||||||
|
echo "✓ Security scans completed"
|
||||||
|
echo "✓ Secrets check passed"
|
||||||
|
echo "✓ Best practices verified"
|
||||||
|
echo ""
|
||||||
|
echo "✓ All checks passed"
|
||||||
+30
-2
@@ -1,5 +1,6 @@
|
|||||||
# Environment files — real values must never be committed
|
# Environment files — real values must never be committed
|
||||||
.env
|
.env
|
||||||
|
.env.terraform.sh
|
||||||
|
|
||||||
# Private CA key and generated TLS certs — ca.key must never enter the cluster or git.
|
# Private CA key and generated TLS certs — ca.key must never enter the cluster or git.
|
||||||
# Only ca.crt is safe to share, but we exclude the whole dir to avoid accidents.
|
# Only ca.crt is safe to share, but we exclude the whole dir to avoid accidents.
|
||||||
@@ -8,6 +9,9 @@ forge/pki/
|
|||||||
# Talos machine configs — contain WireGuard private keys, bootstrap tokens, PKI
|
# Talos machine configs — contain WireGuard private keys, bootstrap tokens, PKI
|
||||||
cluster-config/controlplane.yaml
|
cluster-config/controlplane.yaml
|
||||||
cluster-config/worker*.yaml
|
cluster-config/worker*.yaml
|
||||||
|
cluster-config/talos-worker*.yaml
|
||||||
|
cluster-config/cp-*.yaml
|
||||||
|
cluster-config/talos-cp-*.yaml
|
||||||
cluster-config/secrets.yaml
|
cluster-config/secrets.yaml
|
||||||
cluster-config/talosconfig
|
cluster-config/talosconfig
|
||||||
talos-forge-trust.yaml
|
talos-forge-trust.yaml
|
||||||
@@ -34,7 +38,31 @@ k8s/storage/test/test
|
|||||||
*.key
|
*.key
|
||||||
*.conf
|
*.conf
|
||||||
|
|
||||||
# Allowed markdown: CLAUDE.example.md, README.md, TROUBLESHOOTING.md
|
# CLAUDE.md is now version-controlled (was previously excluded as a
|
||||||
CLAUDE.md
|
# private-notes file; contains no secrets - just architecture, IPs
|
||||||
|
# [private RFC1918 space], and operational lessons, same bar as README.md).
|
||||||
|
|
||||||
|
# Terraform state and cache (local files, remote state in MinIO)
|
||||||
|
.terraform/
|
||||||
|
terraform/.terraform/
|
||||||
|
terraform/*.tfstate
|
||||||
|
terraform/*.tfstate.*
|
||||||
|
terraform.tfvars.local
|
||||||
skills-lock.json
|
skills-lock.json
|
||||||
|
secrets-plaintext.yaml
|
||||||
|
|
||||||
|
# Saved plan files — binary, environment-specific, may embed resource attributes
|
||||||
|
terraform/tfplan
|
||||||
|
terraform/tfplan-*
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
CLAUDE.md
|
||||||
|
docs/
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
creation_rules:
|
||||||
|
# `secrets?` — singular too. A `seed-repo-secret.yaml` once slipped this regex
|
||||||
|
# and was committed in plaintext to a public remote.
|
||||||
|
- path_regex: k8s/.*secrets?.*\.ya?ml
|
||||||
|
age: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
extends: default
|
||||||
|
|
||||||
|
rules:
|
||||||
|
line-length:
|
||||||
|
max: 120
|
||||||
|
level: warning
|
||||||
|
indentation:
|
||||||
|
spaces: 2
|
||||||
|
brackets:
|
||||||
|
min-spaces-inside: 0
|
||||||
|
max-spaces-inside: 0
|
||||||
|
braces:
|
||||||
|
min-spaces-inside: 0
|
||||||
|
max-spaces-inside: 0
|
||||||
|
comments:
|
||||||
|
min-spaces-from-content: 2
|
||||||
|
comments-indentation: {}
|
||||||
|
document-end: disable
|
||||||
|
document-start: disable
|
||||||
|
empty-lines:
|
||||||
|
max: 3
|
||||||
|
empty-values:
|
||||||
|
forbid-in-block-mappings: true
|
||||||
|
forbid-in-flow-mappings: true
|
||||||
|
key-duplicates: enable
|
||||||
|
key-ordering: disable
|
||||||
|
new-line-at-end-of-file: enable
|
||||||
|
new-lines:
|
||||||
|
type: unix
|
||||||
|
trailing-spaces: enable
|
||||||
|
truthy:
|
||||||
|
level: warning
|
||||||
+184
-46
@@ -1,72 +1,210 @@
|
|||||||
# CLAUDE.md — Homelab Integration Guide
|
# CLAUDE.md — Homelab Integration Guide (Example / Reusable Template)
|
||||||
|
|
||||||
**Homelab:** A bare-metal three-node Kubernetes cluster running Talos Linux with a full observability stack, SSO via Authentik, secret management via Vault, and CI/CD infrastructure (Forgejo + Argo CD, deployed).
|
> **This is a sanitized template.** Copy to `CLAUDE.md`, fill in your own
|
||||||
|
> node IPs/hostnames/secrets, and delete this notice. Nothing in this file
|
||||||
|
> should contain real credentials, real IPs beyond illustrative examples, or
|
||||||
|
> anything that would matter if this file became public. It's meant to be
|
||||||
|
> shared across homelabs running similar hardware/topology (3-node bare-metal
|
||||||
|
> Talos Kubernetes + ArgoCD GitOps), not just this one.
|
||||||
|
|
||||||
|
**Homelab:** A bare-metal N-node Kubernetes cluster running Talos Linux with a
|
||||||
|
full observability stack, SSO via Authentik, secret management via Vault, and
|
||||||
|
CI/CD infrastructure (self-hosted git forge + Argo CD).
|
||||||
|
|
||||||
|
## Cluster Topology (adjust to your hardware)
|
||||||
|
|
||||||
|
| Node | IP | Zone | Scheduling | Storage |
|
||||||
|
|------|----|----|-----------|---------|
|
||||||
|
| `<node-1>` | `<ip>` | az-a | schedulable (all workloads) | sole storage node (if single-node storage) |
|
||||||
|
| `<node-2>` | `<ip>` | az-b | dedicated (`NoSchedule`) | none |
|
||||||
|
| `<node-3>` | `<ip>` | az-c | dedicated (`NoSchedule`) | none |
|
||||||
|
|
||||||
|
If your storage layer (Longhorn, local-path, etc.) only runs on one node,
|
||||||
|
every stateful workload pinned to that storage class is effectively
|
||||||
|
single-instance regardless of your control-plane HA count — document that
|
||||||
|
explicitly here, it changes your failure-mode assumptions everywhere else.
|
||||||
|
|
||||||
|
## Deployment Model: ArgoCD GitOps (app-of-apps)
|
||||||
|
|
||||||
|
```
|
||||||
|
git commit → git push (your git forge) → ArgoCD auto-sync → cluster
|
||||||
|
```
|
||||||
|
|
||||||
|
Structure:
|
||||||
|
- One root `Application` (`k8s/argocd/root/`) pointing at a directory of
|
||||||
|
child `Application` manifests (`k8s/argocd/apps/*.yaml`)
|
||||||
|
- Each child Application is either:
|
||||||
|
- A remote Helm chart + a **second** git source (`ref: values`) supplying
|
||||||
|
just the values file — lets you pin an upstream chart version while
|
||||||
|
keeping your values under normal git history/review
|
||||||
|
- A plain git directory of raw manifests (optionally with a
|
||||||
|
`kustomization.yaml`)
|
||||||
|
- `argocd.argoproj.io/sync-wave` annotations control ordering across
|
||||||
|
Applications (lower number syncs first)
|
||||||
|
|
||||||
|
**Never `kubectl apply`/`patch`/`delete` a resource ArgoCD manages**, except:
|
||||||
|
- Pure cleanup of stuck/dead state (e.g. deleting a permanently-failed hook
|
||||||
|
Job so the next real sync can create a fresh one) — this is not a config
|
||||||
|
change, just clearing wreckage that GitOps itself won't clean up
|
||||||
|
automatically (see Gotchas below)
|
||||||
|
- Genuine one-time bootstrap circular dependencies (e.g. Vault
|
||||||
|
`operator init`/unseal — nothing can configure Vault's own unseal keys
|
||||||
|
before Vault has generated them)
|
||||||
|
|
||||||
|
## Hard Rules (adapt freely, but keep something like these)
|
||||||
|
|
||||||
|
🔴 **Identify and document your storage/stateful-singleton node explicitly.**
|
||||||
|
Whatever node holds your CSI driver's data (Longhorn, local-path, etc.),
|
||||||
|
renaming or wiping it orphans every PVC pinned there. Name it here, in
|
||||||
|
caps, so nobody "cleans up" it by accident.
|
||||||
|
|
||||||
|
🔴 **If you run multi-member etcd across a LAN + VPN/WireGuard overlay,
|
||||||
|
pin the advertised subnet explicitly** (e.g. Talos's
|
||||||
|
`cluster.etcd.advertisedSubnets`). Without it, etcd may advertise on the
|
||||||
|
wrong interface and new members hang as non-promoting learners.
|
||||||
|
|
||||||
|
🔴 **Run your IaC formatter (terraform fmt, etc.) before every commit
|
||||||
|
that touches infra code.** Wire this into CI as a hard gate, not a
|
||||||
|
suggestion.
|
||||||
|
|
||||||
|
🔴 **Whatever your source of truth is (Terraform, ArgoCD, both) — never
|
||||||
|
manually mutate resources it manages.** State drift is the single most
|
||||||
|
common cause of "why did my last apply undo my manual fix" confusion.
|
||||||
|
Fix the source, re-apply/re-sync, never bypass.
|
||||||
|
|
||||||
|
🔴 **Never delete a PVC without confirming replica count / backup
|
||||||
|
freshness first.** This is always a one-way door.
|
||||||
|
|
||||||
|
🔴 **Decide your commit message convention up front and enforce it.**
|
||||||
|
(This template's origin project uses: no co-authored-by footers, single-line
|
||||||
|
commit summarizing what/why, solo-authorship assumption — adjust to your
|
||||||
|
team's norms.)
|
||||||
|
|
||||||
|
🔴 **Decide your git workflow (rebase vs merge) up front and stick to it**
|
||||||
|
cluster-wide, across every contributor/agent working in the repo.
|
||||||
|
|
||||||
|
🔴 **Long-running commands should not block a synchronous session** — run
|
||||||
|
them in the background and poll, especially anything that waits on a
|
||||||
|
Kubernetes rollout, an image pull, or a Terraform apply.
|
||||||
|
|
||||||
|
## GitOps / ArgoCD Gotchas (transferable to any ArgoCD-based homelab)
|
||||||
|
|
||||||
|
🟠 **A `kustomization.yaml` with an explicit `resources:` allowlist
|
||||||
|
silently drops anything you forget to list.** No error, no drift shown in
|
||||||
|
ArgoCD's UI — it just reports `Synced/Healthy` against a manifest set that
|
||||||
|
never included your new file. Always run `kubectl kustomize <dir>/`
|
||||||
|
locally before pushing to confirm exactly what ArgoCD will build.
|
||||||
|
|
||||||
|
🟠 **A top-level `namespace:` transformer in `kustomization.yaml` rewrites
|
||||||
|
`metadata.namespace` on every resource it builds** — including RBAC
|
||||||
|
bindings deliberately targeting a *different* namespace (e.g. granting a
|
||||||
|
ServiceAccount in namespace A read access to Secrets in namespace B). If
|
||||||
|
any manifest needs cross-namespace RBAC, either drop the transformer
|
||||||
|
(safe if every resource already sets its own explicit namespace) or give
|
||||||
|
that manifest its own Application/directory.
|
||||||
|
|
||||||
|
🟠 **PreSync hooks run before an Application's own normal resources are
|
||||||
|
synced.** A PreSync Job that depends on RBAC/ServiceAccounts defined as
|
||||||
|
plain (non-hook) resources in the *same* Application will deadlock — it
|
||||||
|
tries to start before its own permissions exist. Use PostSync instead if
|
||||||
|
the hook needs resources from its own Application, or move the
|
||||||
|
prerequisite RBAC into an earlier sync-wave Application.
|
||||||
|
|
||||||
|
🟠 **ArgoCD hooks are not continuously reconciled by `selfHeal`.** Once a
|
||||||
|
hook Job finishes (success, or exhausts `backoffLimit`), it's only
|
||||||
|
deleted+recreated during an *actual new Sync operation* — not by passive
|
||||||
|
drift detection, even with `automated.selfHeal: true` on. If you fix a
|
||||||
|
broken hook's spec and push, the Application's `status.sync.revision` can
|
||||||
|
show "caught up" while the live hook resource is still the old, broken
|
||||||
|
one, because no new operation actually re-ran it. To force it: delete the
|
||||||
|
stuck hook (clear `argocd.argoproj.io/hook-finalizer` manually if it's
|
||||||
|
stuck `Terminating`), and if that alone doesn't trigger a fresh full sync,
|
||||||
|
delete + re-`kubectl apply -f` the Application object itself.
|
||||||
|
|
||||||
|
🟠 **If you route ArgoCD's own `repoURL` through an ingress/reverse-proxy
|
||||||
|
hostname that only listens on 80/443, don't use a non-standard port in the
|
||||||
|
URL** — it'll silently time out trying to reach a port the proxy never
|
||||||
|
opened, and depending on your setup this can block *every* Application's
|
||||||
|
sync simultaneously (repo-server can't fetch git refs for anything).
|
||||||
|
|
||||||
|
🟠 **Don't pin exact version tags for images from registries that don't
|
||||||
|
guarantee tag retention** (Bitnami stopped publishing versioned tags for
|
||||||
|
free-tier images in 2025 — only `latest` + sha256 digests remain). Verify
|
||||||
|
a tag actually exists before pinning it, or prefer minimal base images +
|
||||||
|
a stdlib-only runtime download (e.g. Python's `urllib.request` to fetch a
|
||||||
|
static binary) to avoid depending on any third party's tagging policy.
|
||||||
|
|
||||||
|
🟠 **Non-root containers can't `apk add`/`apt install` in most default
|
||||||
|
base images** — package manager directories are root-owned. Use a
|
||||||
|
world-writable scratch dir (`/tmp`) for anything you need to
|
||||||
|
download/install at runtime instead.
|
||||||
|
|
||||||
|
🟠 **Helm does not validate unknown `values.yaml` keys.** A typo, or a
|
||||||
|
values schema copied from the wrong chart *version's* docs/examples, is
|
||||||
|
silently a no-op — not an error. Before concluding "this chart doesn't
|
||||||
|
support X," clone the chart at your exact pinned version/tag and run
|
||||||
|
`helm template` against your real values file, then diff the rendered
|
||||||
|
output. Don't trust a chart's current `main`-branch example values file
|
||||||
|
if you're pinned to an older release — schemas do change between major
|
||||||
|
versions without warning in your own values file.
|
||||||
|
|
||||||
## Service Integration Routes
|
## Service Integration Routes
|
||||||
|
|
||||||
**New service? Pick your stack below:**
|
**New service? Pick your stack below** (adjust doc paths to match your repo):
|
||||||
|
|
||||||
| Need | Doc | Example |
|
| Need | Doc | Example |
|
||||||
|------|-----|---------|
|
|------|-----|---------|
|
||||||
| **Authentication** | `project-usage/authentik-oidc.md` | OAuth2 login, RBAC groups, JWT tokens |
|
| **Authentication** | `project-usage/authentik-oidc.md` | OAuth2 login, RBAC groups, JWT tokens |
|
||||||
| **Async messaging** | `project-usage/sqs-messaging.md` | Kafka topic consumers, fire-and-forget, DLQ |
|
| **Async messaging** | `project-usage/sqs-messaging.md` | Queue consumers, fire-and-forget, DLQ |
|
||||||
| **Object storage** | `project-usage/minio-s3.md` | File uploads, backups, log backend |
|
| **Object storage** | `project-usage/minio-s3.md` | File uploads, backups, log backend |
|
||||||
| **CI/CD pipeline** | `project-usage/cicd-workflow.md` | GitHub Actions syntax, image push, Argo CD sync |
|
| **CI/CD pipeline** | `project-usage/cicd-workflow.md` | Pipeline syntax, image push, ArgoCD sync |
|
||||||
| **Workflows** | `project-usage/temporal-workflows.md` | Long-running jobs, retries, state machines |
|
| **Workflows** | `project-usage/temporal-workflows.md` | Long-running jobs, retries, state machines |
|
||||||
| **Database** | `project-usage/database-postgres.md` | CloudNativePG setup, schema migrations, replicas |
|
| **Database** | `project-usage/database-postgres.md` | CloudNativePG setup, schema migrations, replicas |
|
||||||
| **Monitoring** | `project-usage/monitoring-metrics.md` | Prometheus scrape, Grafana dashboard, alerts |
|
| **Monitoring** | `project-usage/monitoring-metrics.md` | Prometheus scrape, Grafana dashboard, alerts |
|
||||||
| **Secrets** | `project-usage/vault-secrets.md` | Store credentials, rotate tokens, seal/unseal |
|
| **Secrets** | `project-usage/vault-secrets.md` | Store credentials, rotate tokens, seal/unseal |
|
||||||
| **Networking** | `project-usage/networking-ingress.md` | Public HTTPS, hostname routing, TLS |
|
| **Networking** | `project-usage/networking-ingress.md` | Public HTTPS, hostname routing, TLS |
|
||||||
|
|
||||||
## Cluster Essentials
|
## Cluster Essentials (fill in your own inventory)
|
||||||
|
|
||||||
**22 namespaces, 18 releases:**
|
**Architecture principles (adjust to taste, but these travel well):**
|
||||||
```
|
- Immutable OS (Talos, or similar — no SSH, fully declarative config)
|
||||||
Core: cert-manager, ingress-nginx, kube-system, cilium
|
- Secrets in a proper secrets backend (Vault) + SOPS-encrypted manifests in
|
||||||
Storage: longhorn-system, storage (MinIO)
|
git (`*.enc.yaml`, age-encrypted); never commit plaintext secrets or `.env`
|
||||||
Data: ddb (PostgreSQL), iam (Authentik + Vault)
|
- ArgoCD app-of-apps as the single CD source of truth; two-phase bootstrap
|
||||||
Observability: logging (Loki + Grafana), monitoring (Prometheus)
|
documented separately (chicken-and-egg: ArgoCD needs to exist before it
|
||||||
Apps: cicd (Forgejo + Argo CD), sqs (Kafka + kmsvc), temporal, story-crater-backend
|
can deploy itself declaratively — document your exact bootstrap steps)
|
||||||
```
|
- Pull-based GitOps — no kubeconfig/cluster credentials ever touch your CI
|
||||||
|
runner; the runner only needs push access to git, ArgoCD does the rest
|
||||||
|
- Federated OIDC (one identity provider fronting every service that
|
||||||
|
supports it)
|
||||||
|
|
||||||
**Architecture principles:**
|
## Deployment Checklist (per new service)
|
||||||
- Immutable OS (Talos — no SSH, declarative config)
|
|
||||||
- Secrets in Vault (never commit `.env`, credentials in Secret volumes)
|
|
||||||
- Helmfile = single source of truth (`helmfile.yaml.gotmpl`)
|
|
||||||
- Pull-based GitOps (Argo CD, no kubeconfig in CI)
|
|
||||||
- Federated OIDC (Authentik provider for all services)
|
|
||||||
|
|
||||||
|
- [ ] Prometheus `/metrics` endpoint or ServiceMonitor, if it exposes metrics
|
||||||
## Deployment Checklist
|
- [ ] All credentials in your secrets backend (never in plain values.yaml,
|
||||||
|
pod env directly, or committed anywhere in cleartext)
|
||||||
- [ ] Service has Prometheus `/metrics` endpoint or ServiceMonitor
|
- [ ] Ingress rule with TLS, if externally reachable
|
||||||
- [ ] All credentials in Vault (never in pod env, ConfigMap, or code)
|
- [ ] Dashboard + alert rules, if metrics are exposed
|
||||||
- [ ] Ingress rule in `k8s/ingress/` with TLS cert
|
- [ ] ArgoCD `Application` manifest added to the appropriate sync-wave file,
|
||||||
- [ ] Grafana dashboard in `k8s/monitoring/dashboards/svc-<name>.yaml`
|
**not** a standalone `helm install`/`kubectl apply` run by hand
|
||||||
- [ ] Alert rules in `k8s/monitoring/alerts/svc-<name>-rules.yaml` (if needed)
|
- [ ] Validated locally before push: `kubectl apply --dry-run=client -f`,
|
||||||
- [ ] Helm release in `helmfile.yaml.gotmpl` with correct `needs:` dependencies
|
`kubectl kustomize <dir>/` (if applicable), or `helm template` against
|
||||||
|
the exact pinned chart version (if Helm-sourced)
|
||||||
## Hard Rules
|
- [ ] After push: confirmed ArgoCD's `status.sync.revision` actually matches
|
||||||
|
your new commit — not just that `status.sync.status` says `Synced`
|
||||||
1. **No kubeconfig in CI** — Argo CD bridges gap (pull-based, never push secrets to runner)
|
(see Gotchas — a stale hook can hide behind an otherwise-current app)
|
||||||
2. **Field name = variable name** — In Vault: `talos put cluster/KAFKA_BOOTSTRAP KAFKA_BOOTSTRAP="..."`
|
|
||||||
3. **Secrets via volumes** — Never `--env` flag in pod specs (exposes in `kubectl describe`)
|
|
||||||
4. **External services via Ingress** — All public endpoints via TLS (homelab-ca)
|
|
||||||
5. **Never commit `.env`** — Only `.env.example` in git; real secrets in Vault
|
|
||||||
|
|
||||||
## Git & Release
|
## Git & Release
|
||||||
|
|
||||||
**Multi-remote push:**
|
**Incremental commits (service-layer grouped) tend to age well:**
|
||||||
```bash
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
**Incremental commits (service-layer grouped):**
|
|
||||||
- Foundation & Docs
|
- Foundation & Docs
|
||||||
- Helmfile & Core Infra
|
- Core Infra (CNI, ingress, cert management, storage)
|
||||||
- Storage Layer
|
|
||||||
- Observability Stack
|
- Observability Stack
|
||||||
- IAM & Secrets
|
- IAM & Secrets
|
||||||
- CI/CD & GitOps
|
- CI/CD & GitOps
|
||||||
- Messaging Infrastructure
|
- Messaging / Data Infrastructure
|
||||||
- Applications & Utilities
|
- Applications & Utilities
|
||||||
|
|
||||||
|
Grouping by layer (rather than by day or by "misc fixes") makes it much
|
||||||
|
easier to `git log --oneline -- <path>` your way back to *why* a given
|
||||||
|
piece of config looks the way it does, months later.
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
# ── Node IPs ──────────────────────────────────────────────────────────────────
|
# ── Node IPs (3-CP HA topology) ───────────────────────────────────────────────
|
||||||
# CP_IP has a default. All W{N}_IP variables are expected to be exported from
|
CP1_IP := 192.168.1.166 # talos-cp-1
|
||||||
# ~/.zshrc (e.g. export W1_IP=192.168.1.162). No guards — assumed always set.
|
CP2_IP := 192.168.1.214 # talos-cp-2 (storage: 3 disks)
|
||||||
CP_IP ?= 192.168.1.213
|
CP3_IP := 192.168.1.162 # talos-cp-3
|
||||||
|
CP_VIP := 192.168.1.166 # controlplane VIP (currently .166)
|
||||||
export CP_IP
|
|
||||||
|
|
||||||
# ── Paths ─────────────────────────────────────────────────────────────────────
|
# ── Paths ─────────────────────────────────────────────────────────────────────
|
||||||
TALOSCONFIG := cluster-config/coreconfig
|
TALOSCONFIG := cluster-config/talosconfig
|
||||||
CP_CONFIG := cluster-config/controlplane.yaml
|
CP1_CONFIG := cluster-config/talos-cp-1.yaml
|
||||||
SECRETS := cluster-config/secrets.yaml
|
CP2_CONFIG := cluster-config/talos-cp-2.yaml
|
||||||
|
CP3_CONFIG := cluster-config/talos-cp-3.yaml
|
||||||
KUBECONFIG := cluster-config/kubeconfig
|
KUBECONFIG := cluster-config/kubeconfig
|
||||||
|
|
||||||
CLUSTER_NAME := homelab-cluster
|
CLUSTER_NAME := homelab
|
||||||
CP_ENDPOINT := https://$(CP_IP):6443
|
CP_ENDPOINT := https://$(CP_VIP):6443
|
||||||
TALOS_IMAGE := factory.core.dev/installer/613e1592b2da41ae5e265e8789429f22e121aab91cb4deb6bc3c0b6262961245:v1.13.3
|
|
||||||
|
|
||||||
TALOSCTL := corectl --coreconfig $(TALOSCONFIG)
|
# Use talosctl (not corectl). Needs TALOSCONFIG env var pointing to talosconfig file.
|
||||||
|
TALOSCTL := talosctl
|
||||||
KUBECTL := kubectl --kubeconfig $(KUBECONFIG)
|
KUBECTL := kubectl --kubeconfig $(KUBECONFIG)
|
||||||
|
|
||||||
# Derive IP and config from worker number N (used by generic targets).
|
# Derive IP and config from worker number N (used by generic targets).
|
||||||
@@ -28,236 +28,157 @@ W_CONFIG = cluster-config/worker-$(N).yaml
|
|||||||
# ── Help ──────────────────────────────────────────────────────────────────────
|
# ── Help ──────────────────────────────────────────────────────────────────────
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
help:
|
help:
|
||||||
@echo "Homelab cluster — available targets"
|
@echo "Homelab cluster (3-CP HA: .166/.214/.162) — available targets"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " Status"
|
@echo " Status & Services"
|
||||||
@echo " nodes kubectl get nodes"
|
@echo " nodes kubectl get nodes"
|
||||||
@echo " status-cp core node overview (control plane)"
|
@echo " status-all etcd members on all 3 CPs"
|
||||||
@echo " status-w1 core node overview (worker-1)"
|
@echo " status-cp1/2/3 etcd members on specific CP"
|
||||||
@echo " services-cp list core services (control plane)"
|
@echo " services-cp1/2/3 list Talos services on specific CP"
|
||||||
@echo " services-w1 list core services (worker-1)"
|
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " Logs"
|
@echo " Logs"
|
||||||
@echo " logs-cp stream kubelet logs (control plane)"
|
@echo " logs-cp1/2/3 stream kubelet logs from CP{1,2,3}"
|
||||||
@echo " logs-w1 stream kubelet logs (worker-1)"
|
@echo " dmesg-cp1/2/3 stream kernel dmesg from CP{1,2,3}"
|
||||||
@echo " dmesg-cp kernel dmesg (control plane)"
|
@echo " log-svc-cp1/2/3 stream service logs (SVC=<name>)"
|
||||||
@echo " dmesg-w1 kernel dmesg (worker-1)"
|
|
||||||
@echo " log-svc-cp stream a service log (control plane) SVC=<name>"
|
|
||||||
@echo " log-svc-w1 stream a service log (worker-1) SVC=<name>"
|
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " Config"
|
@echo " Config Apply"
|
||||||
@echo " gen-config regenerate controlplane.yaml + worker-N.yaml from secrets"
|
@echo " apply-all apply configs to all 3 CPs (talos-cp-{1,2,3}.yaml)"
|
||||||
@echo " apply-cp apply controlplane.yaml to CP node (live cluster)"
|
@echo " apply-cp1/2/3 apply config to specific CP"
|
||||||
@echo " apply-w1 apply cluster-config/worker-1.yaml to worker-1"
|
|
||||||
@echo " apply-w1-insecure first-time apply to worker-1 (no certs yet)"
|
|
||||||
@echo " apply-worker apply cluster-config/worker-N.yaml N=<num> W<N>_IP=<ip>"
|
|
||||||
@echo " apply-worker-new first-time apply (--insecure) N=<num> W<N>_IP=<ip>"
|
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " Upgrade"
|
@echo " Reboot"
|
||||||
@echo " upgrade-cp upgrade Talos on control plane"
|
@echo " reboot-all reboot all 3 CPs"
|
||||||
@echo " upgrade-w1 upgrade Talos on worker-1"
|
@echo " reboot-cp1/2/3 reboot specific CP"
|
||||||
@echo " upgrade-worker upgrade any worker N=<num> W<N>_IP=<ip>"
|
|
||||||
@echo ""
|
|
||||||
@echo " Shutdown / Reboot"
|
|
||||||
@echo " shutdown-cluster graceful full shutdown (drain w1 → off w1 → off cp)"
|
|
||||||
@echo " shutdown-cp shut down control plane only"
|
|
||||||
@echo " shutdown-w1 shut down worker-1 only"
|
|
||||||
@echo " shutdown-worker shut down any worker N=<num> W<N>_IP=<ip>"
|
|
||||||
@echo " reboot-cp reboot control plane"
|
|
||||||
@echo " reboot-w1 reboot worker-1"
|
|
||||||
@echo " reboot-worker reboot any worker N=<num> W<N>_IP=<ip>"
|
|
||||||
@echo ""
|
|
||||||
@echo " Inspect (node filesystem)"
|
|
||||||
@echo " node-ls <ip> <path> list files on a node"
|
|
||||||
@echo " node-read <ip> <path> read a file on a node"
|
|
||||||
@echo ""
|
|
||||||
@echo " Maintenance"
|
|
||||||
@echo " clean-pods delete Evicted/Failed/Terminating pods cluster-wide"
|
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " Port-forwards"
|
@echo " Port-forwards"
|
||||||
@echo " pf-grafana localhost:3000 → Grafana"
|
@echo " pf-grafana localhost:3000 → Grafana"
|
||||||
@echo " pf-minio localhost:9001 → MinIO console / localhost:9000 → S3 API"
|
|
||||||
@echo " pf-loki localhost:3100 → Loki HTTP API"
|
|
||||||
@echo " pf-portainer localhost:9000 → Portainer UI (dashboard ns)"
|
|
||||||
@echo " pf-prometheus localhost:9090 → Prometheus UI (monitoring ns)"
|
|
||||||
@echo " pf-longhorn localhost:8080 → Longhorn UI"
|
@echo " pf-longhorn localhost:8080 → Longhorn UI"
|
||||||
@echo " pf-iam localhost:7000 → Authentik IAM (when deployed)"
|
@echo " pf-prometheus localhost:9090 → Prometheus UI"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " CLI"
|
@echo " IPs"
|
||||||
@echo " cli build core-cli and install to ~/.local/bin/core"
|
@echo " CP1 (talos-cp-1): $(CP1_IP) — NVMe, wg0/wg1, VIP"
|
||||||
@echo ""
|
@echo " CP2 (talos-cp-2): $(CP2_IP) — 3 Longhorn disks"
|
||||||
@echo " Variables"
|
@echo " CP3 (talos-cp-3): $(CP3_IP) — NVMe"
|
||||||
@echo " CP_IP (default: 192.168.1.160)"
|
|
||||||
@echo " W1_IP (export from ~/.zshrc — e.g. export W1_IP=192.168.1.162)"
|
|
||||||
@echo " N (required for generic targets — worker number, e.g. N=2)"
|
|
||||||
@echo " W<N>_IP (export from ~/.zshrc — e.g. export W2_IP=192.168.1.163)"
|
|
||||||
@echo " SVC (required for log-svc-* targets, e.g. SVC=kubelet)"
|
|
||||||
|
|
||||||
# ── Status ────────────────────────────────────────────────────────────────────
|
# ── Status ────────────────────────────────────────────────────────────────────
|
||||||
.PHONY: nodes
|
.PHONY: nodes
|
||||||
nodes:
|
nodes:
|
||||||
$(KUBECTL) get nodes -o wide
|
$(KUBECTL) get nodes -o wide
|
||||||
|
|
||||||
|
.PHONY: status-all
|
||||||
|
status-all: status-cp1 status-cp2 status-cp3
|
||||||
|
|
||||||
|
.PHONY: status-cp1
|
||||||
|
status-cp1:
|
||||||
|
$(TALOSCTL) -n $(CP1_IP) --endpoints $(CP1_IP) etcd members
|
||||||
|
|
||||||
|
.PHONY: status-cp2
|
||||||
|
status-cp2:
|
||||||
|
$(TALOSCTL) -n $(CP2_IP) --endpoints $(CP2_IP) etcd members
|
||||||
|
|
||||||
|
.PHONY: status-cp3
|
||||||
|
status-cp3:
|
||||||
|
$(TALOSCTL) -n $(CP3_IP) --endpoints $(CP3_IP) etcd members
|
||||||
|
|
||||||
.PHONY: status-cp
|
.PHONY: status-cp
|
||||||
status-cp:
|
status-cp: status-all
|
||||||
$(TALOSCTL) --nodes $(CP_IP) get members
|
|
||||||
|
|
||||||
.PHONY: status-w1
|
.PHONY: services-cp1
|
||||||
status-w1:
|
services-cp1:
|
||||||
$(TALOSCTL) --nodes $(W1_IP) get members
|
$(TALOSCTL) -n $(CP1_IP) --endpoints $(CP1_IP) service
|
||||||
|
|
||||||
.PHONY: services-cp
|
.PHONY: services-cp2
|
||||||
services-cp:
|
services-cp2:
|
||||||
$(TALOSCTL) --nodes $(CP_IP) service
|
$(TALOSCTL) -n $(CP2_IP) --endpoints $(CP2_IP) service
|
||||||
|
|
||||||
.PHONY: services-w1
|
.PHONY: services-cp3
|
||||||
services-w1:
|
services-cp3:
|
||||||
$(TALOSCTL) --nodes $(W1_IP) service
|
$(TALOSCTL) -n $(CP3_IP) --endpoints $(CP3_IP) service
|
||||||
|
|
||||||
# ── Logs ──────────────────────────────────────────────────────────────────────
|
# ── Logs (3-CP) ───────────────────────────────────────────────────────────────
|
||||||
.PHONY: logs-cp
|
.PHONY: logs-cp1
|
||||||
logs-cp:
|
logs-cp1:
|
||||||
$(TALOSCTL) --nodes $(CP_IP) logs kubelet -f
|
$(TALOSCTL) -n $(CP1_IP) --endpoints $(CP1_IP) logs kubelet -f
|
||||||
|
|
||||||
.PHONY: logs-w1
|
.PHONY: logs-cp2
|
||||||
logs-w1:
|
logs-cp2:
|
||||||
$(TALOSCTL) --nodes $(W1_IP) logs kubelet -f
|
$(TALOSCTL) -n $(CP2_IP) --endpoints $(CP2_IP) logs kubelet -f
|
||||||
|
|
||||||
.PHONY: dmesg-cp
|
.PHONY: logs-cp3
|
||||||
dmesg-cp:
|
logs-cp3:
|
||||||
$(TALOSCTL) --nodes $(CP_IP) dmesg --follow
|
$(TALOSCTL) -n $(CP3_IP) --endpoints $(CP3_IP) logs kubelet -f
|
||||||
|
|
||||||
.PHONY: dmesg-w1
|
.PHONY: dmesg-cp1
|
||||||
dmesg-w1:
|
dmesg-cp1:
|
||||||
$(TALOSCTL) --nodes $(W1_IP) dmesg --follow
|
$(TALOSCTL) -n $(CP1_IP) --endpoints $(CP1_IP) dmesg --follow
|
||||||
|
|
||||||
# Usage: make log-svc-cp SVC=etcd
|
.PHONY: dmesg-cp2
|
||||||
.PHONY: log-svc-cp
|
dmesg-cp2:
|
||||||
log-svc-cp:
|
$(TALOSCTL) -n $(CP2_IP) --endpoints $(CP2_IP) dmesg --follow
|
||||||
|
|
||||||
|
.PHONY: dmesg-cp3
|
||||||
|
dmesg-cp3:
|
||||||
|
$(TALOSCTL) -n $(CP3_IP) --endpoints $(CP3_IP) dmesg --follow
|
||||||
|
|
||||||
|
# Usage: make log-svc-cp1 SVC=etcd
|
||||||
|
.PHONY: log-svc-cp1
|
||||||
|
log-svc-cp1:
|
||||||
ifndef SVC
|
ifndef SVC
|
||||||
$(error SVC is not set — run: make log-svc-cp SVC=<service-name>)
|
$(error SVC is not set — run: make log-svc-cp1 SVC=<service-name>)
|
||||||
endif
|
endif
|
||||||
$(TALOSCTL) --nodes $(CP_IP) logs $(SVC) -f
|
$(TALOSCTL) -n $(CP1_IP) --endpoints $(CP1_IP) logs $(SVC) -f
|
||||||
|
|
||||||
.PHONY: log-svc-w1
|
.PHONY: log-svc-cp2
|
||||||
log-svc-w1:
|
log-svc-cp2:
|
||||||
ifndef SVC
|
ifndef SVC
|
||||||
$(error SVC is not set — run: make log-svc-w1 SVC=<service-name>)
|
$(error SVC is not set — run: make log-svc-cp2 SVC=<service-name>)
|
||||||
endif
|
endif
|
||||||
$(TALOSCTL) --nodes $(W1_IP) logs $(SVC) -f
|
$(TALOSCTL) -n $(CP2_IP) --endpoints $(CP2_IP) logs $(SVC) -f
|
||||||
|
|
||||||
# ── Config generation ─────────────────────────────────────────────────────────
|
.PHONY: log-svc-cp3
|
||||||
.PHONY: gen-config
|
log-svc-cp3:
|
||||||
gen-config:
|
ifndef SVC
|
||||||
corectl gen config $(CLUSTER_NAME) $(CP_ENDPOINT) \
|
$(error SVC is not set — run: make log-svc-cp3 SVC=<service-name>)
|
||||||
--with-secrets $(SECRETS) \
|
endif
|
||||||
--output-dir cluster-config/ \
|
$(TALOSCTL) -n $(CP3_IP) --endpoints $(CP3_IP) logs $(SVC) -f
|
||||||
--force
|
|
||||||
|
# ── Config Apply (3-CP) ───────────────────────────────────────────────────────
|
||||||
|
.PHONY: apply-all
|
||||||
|
apply-all: apply-cp1 apply-cp2 apply-cp3
|
||||||
|
@echo "✓ All 3 control planes configured"
|
||||||
|
|
||||||
|
.PHONY: apply-cp1
|
||||||
|
apply-cp1:
|
||||||
|
$(TALOSCTL) -n $(CP1_IP) --endpoints $(CP1_IP) apply-config -f $(CP1_CONFIG)
|
||||||
|
|
||||||
|
.PHONY: apply-cp2
|
||||||
|
apply-cp2:
|
||||||
|
$(TALOSCTL) -n $(CP2_IP) --endpoints $(CP2_IP) apply-config -f $(CP2_CONFIG)
|
||||||
|
|
||||||
|
.PHONY: apply-cp3
|
||||||
|
apply-cp3:
|
||||||
|
$(TALOSCTL) -n $(CP3_IP) --endpoints $(CP3_IP) apply-config -f $(CP3_CONFIG)
|
||||||
|
|
||||||
# ── Config apply ──────────────────────────────────────────────────────────────
|
|
||||||
.PHONY: apply-cp
|
.PHONY: apply-cp
|
||||||
apply-cp:
|
apply-cp: apply-all
|
||||||
$(TALOSCTL) apply-config \
|
|
||||||
--nodes $(CP_IP) \
|
|
||||||
--file $(CP_CONFIG)
|
|
||||||
|
|
||||||
.PHONY: apply-w1
|
.PHONY: reboot-all
|
||||||
apply-w1:
|
reboot-all: reboot-cp1 reboot-cp2 reboot-cp3
|
||||||
$(TALOSCTL) apply-config \
|
@echo "✓ All 3 control planes rebooting"
|
||||||
--nodes $(W1_IP) \
|
|
||||||
--file cluster-config/worker-1.yaml
|
|
||||||
|
|
||||||
# First-time apply to worker-1 (no certs yet)
|
.PHONY: reboot-cp1
|
||||||
.PHONY: apply-w1-insecure
|
reboot-cp1:
|
||||||
apply-w1-insecure:
|
$(TALOSCTL) -n $(CP1_IP) --endpoints $(CP1_IP) reboot
|
||||||
$(TALOSCTL) apply-config \
|
|
||||||
--nodes $(W1_IP) \
|
|
||||||
--file cluster-config/worker-1.yaml \
|
|
||||||
--insecure
|
|
||||||
|
|
||||||
# Generic targets — derive both IP and config from N.
|
.PHONY: reboot-cp2
|
||||||
# Usage: make apply-worker N=2 W2_IP=192.168.1.162
|
reboot-cp2:
|
||||||
# make apply-worker N=3 W3_IP=192.168.1.163
|
$(TALOSCTL) -n $(CP2_IP) --endpoints $(CP2_IP) reboot
|
||||||
.PHONY: apply-worker
|
|
||||||
apply-worker:
|
|
||||||
ifndef N
|
|
||||||
$(error N is not set — run: make apply-worker N=<num> W<N>_IP=<ip>)
|
|
||||||
endif
|
|
||||||
$(TALOSCTL) apply-config \
|
|
||||||
--nodes $(W_IP) \
|
|
||||||
--file $(W_CONFIG)
|
|
||||||
|
|
||||||
.PHONY: apply-worker-new
|
.PHONY: reboot-cp3
|
||||||
apply-worker-new:
|
reboot-cp3:
|
||||||
ifndef N
|
$(TALOSCTL) -n $(CP3_IP) --endpoints $(CP3_IP) reboot
|
||||||
$(error N is not set — run: make apply-worker-new N=<num> W<N>_IP=<ip>)
|
|
||||||
endif
|
|
||||||
$(TALOSCTL) apply-config \
|
|
||||||
--nodes $(W_IP) \
|
|
||||||
--file $(W_CONFIG) \
|
|
||||||
--insecure
|
|
||||||
|
|
||||||
# ── Upgrade ───────────────────────────────────────────────────────────────────
|
|
||||||
.PHONY: upgrade-cp
|
|
||||||
upgrade-cp:
|
|
||||||
$(TALOSCTL) upgrade \
|
|
||||||
--nodes $(CP_IP) \
|
|
||||||
--image $(TALOS_IMAGE) \
|
|
||||||
--preserve
|
|
||||||
|
|
||||||
.PHONY: upgrade-w1
|
|
||||||
upgrade-w1:
|
|
||||||
$(TALOSCTL) upgrade \
|
|
||||||
--nodes $(W1_IP) \
|
|
||||||
--image $(TALOS_IMAGE) \
|
|
||||||
--preserve
|
|
||||||
|
|
||||||
# Usage: make upgrade-worker N=2 W2_IP=192.168.1.162
|
|
||||||
.PHONY: upgrade-worker
|
|
||||||
upgrade-worker:
|
|
||||||
ifndef N
|
|
||||||
$(error N is not set — run: make upgrade-worker N=<num> W<N>_IP=<ip>)
|
|
||||||
endif
|
|
||||||
$(TALOSCTL) upgrade \
|
|
||||||
--nodes $(W_IP) \
|
|
||||||
--image $(TALOS_IMAGE) \
|
|
||||||
--preserve
|
|
||||||
|
|
||||||
# ── Shutdown / Reboot ─────────────────────────────────────────────────────────
|
|
||||||
# Full cluster: drain workers first so pods stop cleanly, then workers off,
|
|
||||||
# then CP last (etcd must be the final process to stop).
|
|
||||||
.PHONY: shutdown-cluster
|
|
||||||
shutdown-cluster:
|
|
||||||
@echo "--- draining core-worker-1 ---"
|
|
||||||
$(KUBECTL) drain core-worker-1 --ignore-daemonsets --delete-emptydir-data
|
|
||||||
@echo "--- shutting down worker-1 ---"
|
|
||||||
$(TALOSCTL) --nodes $(W1_IP) shutdown
|
|
||||||
@echo "--- shutting down control plane (last) ---"
|
|
||||||
$(TALOSCTL) --nodes $(CP_IP) shutdown
|
|
||||||
|
|
||||||
.PHONY: shutdown-cp
|
|
||||||
shutdown-cp:
|
|
||||||
$(TALOSCTL) --nodes $(CP_IP) shutdown
|
|
||||||
|
|
||||||
.PHONY: shutdown-w1
|
|
||||||
shutdown-w1:
|
|
||||||
$(TALOSCTL) --nodes $(W1_IP) shutdown
|
|
||||||
|
|
||||||
# Usage: make shutdown-worker N=2 W2_IP=192.168.1.162
|
|
||||||
.PHONY: shutdown-worker
|
|
||||||
shutdown-worker:
|
|
||||||
ifndef N
|
|
||||||
$(error N is not set — run: make shutdown-worker N=<num> W<N>_IP=<ip>)
|
|
||||||
endif
|
|
||||||
$(TALOSCTL) --nodes $(W_IP) shutdown
|
|
||||||
|
|
||||||
.PHONY: reboot-cp
|
.PHONY: reboot-cp
|
||||||
reboot-cp:
|
reboot-cp: reboot-all
|
||||||
$(TALOSCTL) --nodes $(CP_IP) reboot
|
|
||||||
|
|
||||||
.PHONY: reboot-w1
|
|
||||||
reboot-w1:
|
|
||||||
$(TALOSCTL) --nodes $(W1_IP) reboot
|
|
||||||
|
|
||||||
# Usage: make reboot-worker N=2 W2_IP=192.168.1.162
|
# Usage: make reboot-worker N=2 W2_IP=192.168.1.162
|
||||||
.PHONY: reboot-worker
|
.PHONY: reboot-worker
|
||||||
@@ -268,18 +189,16 @@ endif
|
|||||||
$(TALOSCTL) --nodes $(W_IP) reboot
|
$(TALOSCTL) --nodes $(W_IP) reboot
|
||||||
|
|
||||||
# ── Inspect ───────────────────────────────────────────────────────────────────
|
# ── Inspect ───────────────────────────────────────────────────────────────────
|
||||||
# Positional args: make node-ls 192.168.1.160 /etc/kubernetes/manifests
|
# Positional args: make node-ls 192.168.1.166 /etc/kubernetes/manifests
|
||||||
# $(word 2/3, $(MAKECMDGOALS)) captures the extra words; the % rule absorbs
|
|
||||||
# them so Make doesn't error with "No rule to make target".
|
|
||||||
.PHONY: node-ls
|
.PHONY: node-ls
|
||||||
node-ls:
|
node-ls:
|
||||||
$(TALOSCTL) --nodes $(word 2,$(MAKECMDGOALS)) ls $(word 3,$(MAKECMDGOALS))
|
$(TALOSCTL) -n $(word 2,$(MAKECMDGOALS)) --endpoints $(word 2,$(MAKECMDGOALS)) ls $(word 3,$(MAKECMDGOALS))
|
||||||
|
|
||||||
.PHONY: node-read
|
.PHONY: node-read
|
||||||
node-read:
|
node-read:
|
||||||
$(TALOSCTL) --nodes $(word 2,$(MAKECMDGOALS)) read $(word 3,$(MAKECMDGOALS))
|
$(TALOSCTL) -n $(word 2,$(MAKECMDGOALS)) --endpoints $(word 2,$(MAKECMDGOALS)) read $(word 3,$(MAKECMDGOALS))
|
||||||
|
|
||||||
# Absorb positional arguments passed to node-ls / node-read
|
# Absorb positional arguments
|
||||||
%:
|
%:
|
||||||
@:
|
@:
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ Edit `.env` and fill in cluster configuration. See `.env.example` for all option
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Cluster configuration
|
# Cluster configuration
|
||||||
CLUSTER_DOMAIN=riotpiao.homelab.com # Your cluster domain
|
CLUSTER_DOMAIN=riotpiao.com # Your cluster domain
|
||||||
POSTGRES_HOST=ddb-cluster-rw.ddb.svc.cluster.local
|
POSTGRES_HOST=ddb-cluster-rw.ddb.svc.cluster.local
|
||||||
MINIO_ENDPOINT=minio.storage.svc.cluster.local:9000
|
MINIO_ENDPOINT=minio.storage.svc.cluster.local:9000
|
||||||
KAFKA_BOOTSTRAP=kmsvc-kafka-bootstrap.sqs.svc.cluster.local:9092
|
KAFKA_BOOTSTRAP=kmsvc-kafka-bootstrap.sqs.svc.cluster.local:9092
|
||||||
@@ -285,26 +285,39 @@ Add to `/etc/hosts` on every client machine (Mac/Linux):
|
|||||||
|
|
||||||
```
|
```
|
||||||
# WireGuard access (remote — via talos-cp-1)
|
# WireGuard access (remote — via talos-cp-1)
|
||||||
10.6.0.1 grafana.riotpiao.homelab.com authentik.riotpiao.homelab.com vault.riotpiao.homelab.com minio.riotpiao.homelab.com prometheus.riotpiao.homelab.com portainer.riotpiao.homelab.com longhorn.riotpiao.homelab.com loki.riotpiao.homelab.com forgejo.riotpiao.homelab.com
|
10.6.0.1 grafana.riotpiao.com authentik.riotpiao.com vault.riotpiao.com minio.riotpiao.com prometheus.riotpiao.com portainer.riotpiao.com longhorn.riotpiao.com loki.riotpiao.com forgejo.riotpiao.com temporal.riotpiao.com temporal-grpc.riotpiao.com kmsvc.riotpiao.com
|
||||||
|
|
||||||
# LAN access (on the home network — use actual LoadBalancer IP from above)
|
# LAN access (on the home network — use actual LoadBalancer IP from above)
|
||||||
192.168.1.160 grafana.riotpiao.homelab.com authentik.riotpiao.homelab.com vault.riotpiao.homelab.com minio.riotpiao.homelab.com prometheus.riotpiao.homelab.com portainer.riotpiao.homelab.com longhorn.riotpiao.homelab.com loki.riotpiao.homelab.com forgejo.riotpiao.homelab.com
|
192.168.1.160 grafana.riotpiao.com authentik.riotpiao.com vault.riotpiao.com minio.riotpiao.com prometheus.riotpiao.com portainer.riotpiao.com longhorn.riotpiao.com loki.riotpiao.com forgejo.riotpiao.com temporal.riotpiao.com temporal-grpc.riotpiao.com kmsvc.riotpiao.com
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note:** `192.168.1.160` is an example Cilium LB-IPAM assignment. Verify with `kubectl get svc -n ingress-nginx ingress-nginx`.
|
**Note:** `192.168.1.160` is an example Cilium LB-IPAM assignment. Verify with `kubectl get svc -n ingress-nginx ingress-nginx`.
|
||||||
|
|
||||||
|
**There is no real DNS wildcard for `*.riotpiao.com`** — every hostname must be added to `/etc/hosts` explicitly (as above) before it resolves. Adding a new Ingress host doesn't make it reachable by itself; add the line too.
|
||||||
|
|
||||||
|
### kubectl Context
|
||||||
|
|
||||||
|
Two contexts exist in `cluster-config/kubeconfig`, pointed at the same cluster over different paths:
|
||||||
|
|
||||||
|
| Context | Server | Use when |
|
||||||
|
|---|---|---|
|
||||||
|
| `admin@homelab-cluster` | `192.168.1.213:6443` (LAN) | On the home network |
|
||||||
|
| `admin@homelab-cluster-1` | `10.6.0.1:6443` (WireGuard) | Remote / off-LAN |
|
||||||
|
|
||||||
|
If `kubectl` commands hang or refuse the connection, switch: `kubectl config use-context admin@homelab-cluster-1`.
|
||||||
|
|
||||||
Then access services at:
|
Then access services at:
|
||||||
|
|
||||||
| Service | URL | Credentials |
|
| Service | URL | Credentials |
|
||||||
|---------|-----|-------------|
|
|---------|-----|-------------|
|
||||||
| Grafana | http://grafana.riotpiao.homelab.com | admin / `GRAFANA_ADMIN_PASSWORD` or Authentik SSO |
|
| Grafana | http://grafana.riotpiao.com | admin / `GRAFANA_ADMIN_PASSWORD` or Authentik SSO |
|
||||||
| Authentik | http://authentik.riotpiao.homelab.com | akadmin / see `.env` |
|
| Authentik | http://authentik.riotpiao.com | akadmin / see `.env` |
|
||||||
| Vault | http://vault.riotpiao.homelab.com | root token / see `setup_vault.sh` output |
|
| Vault | http://vault.riotpiao.com | root token / see `setup_vault.sh` output |
|
||||||
| MinIO console | http://minio.riotpiao.homelab.com | `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD` |
|
| MinIO console | http://minio.riotpiao.com | `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD` |
|
||||||
| Prometheus | http://prometheus.riotpiao.homelab.com | no auth |
|
| Prometheus | http://prometheus.riotpiao.com | no auth |
|
||||||
| Portainer | http://portainer.riotpiao.homelab.com | set on first visit |
|
| Portainer | http://portainer.riotpiao.com | set on first visit |
|
||||||
| Longhorn | http://longhorn.riotpiao.homelab.com | no auth |
|
| Longhorn | http://longhorn.riotpiao.com | no auth |
|
||||||
| Forgejo *(planned)* | https://forgejo.forge.riotpiao.homelab.com | `rock` / `FORGEJO_ADMIN_PASSWORD`, or Authentik SSO |
|
| Forgejo *(planned)* | https://forgejo.forge.riotpiao.com | `rock` / `FORGEJO_ADMIN_PASSWORD`, or Authentik SSO |
|
||||||
| Argo CD *(planned)* | `kubectl port-forward -n argocd svc/argocd-server 8080:443` | Authentik SSO (admins only) |
|
| Argo CD *(planned)* | `kubectl port-forward -n argocd svc/argocd-server 8080:443` | Authentik SSO (admins only) |
|
||||||
|
|
||||||
Grafana → "Homelab" folder has the operator dashboards (sidecar-loaded from `k8s/monitoring/dashboards/`, no restart needed on change):
|
Grafana → "Homelab" folder has the operator dashboards (sidecar-loaded from `k8s/monitoring/dashboards/`, no restart needed on change):
|
||||||
@@ -366,7 +379,7 @@ Authentik is the central OIDC identity provider. Vault stores secrets and delega
|
|||||||
│
|
│
|
||||||
│ OAuth2 / OIDC
|
│ OAuth2 / OIDC
|
||||||
▼
|
▼
|
||||||
Authentik (authentik.riotpiao.homelab.com)
|
Authentik (authentik.riotpiao.com)
|
||||||
├── grafana app → Grafana OIDC login (group → Admin/Viewer role)
|
├── grafana app → Grafana OIDC login (group → Admin/Viewer role)
|
||||||
├── minio app → MinIO OIDC login (group → readwrite/readonly policy)
|
├── minio app → MinIO OIDC login (group → readwrite/readonly policy)
|
||||||
├── vault-browser → Vault UI OIDC login / `vault login -method=oidc`
|
├── vault-browser → Vault UI OIDC login / `vault login -method=oidc`
|
||||||
@@ -374,13 +387,13 @@ Authentik is the central OIDC identity provider. Vault stores secrets and delega
|
|||||||
│
|
│
|
||||||
│ JWKS endpoint for JWT validation
|
│ JWKS endpoint for JWT validation
|
||||||
▼
|
▼
|
||||||
HashiCorp Vault (vault.riotpiao.homelab.com)
|
HashiCorp Vault (vault.riotpiao.com)
|
||||||
├── auth/jwt — core-cli authenticates with device code JWT
|
├── auth/jwt — core-cli authenticates with device code JWT
|
||||||
├── auth/oidc — browser/UI login via Authentik
|
├── auth/oidc — browser/UI login via Authentik
|
||||||
└── secret/ — KV v2: mcp/*, cluster/*, cloud/*
|
└── secret/ — KV v2: mcp/*, cluster/*, cloud/*
|
||||||
```
|
```
|
||||||
|
|
||||||
**talos-cli device code login:**
|
**core CLI device code login:**
|
||||||
```bash
|
```bash
|
||||||
core secrets login # prints URL + code → approve in browser → Vault token cached
|
core secrets login # prints URL + code → approve in browser → Vault token cached
|
||||||
core put cluster/DUCKDNS_TOKEN DUCKDNS_TOKEN="abc" # field name = variable name, never `value`
|
core put cluster/DUCKDNS_TOKEN DUCKDNS_TOKEN="abc" # field name = variable name, never `value`
|
||||||
@@ -389,8 +402,8 @@ core put cluster/DUCKDNS_TOKEN DUCKDNS_TOKEN="abc" # field name = var
|
|||||||
**One-time IAM setup (after `helmfile apply`):**
|
**One-time IAM setup (after `helmfile apply`):**
|
||||||
```bash
|
```bash
|
||||||
# 1. Provision OIDC apps and groups in Authentik
|
# 1. Provision OIDC apps and groups in Authentik
|
||||||
GRAFANA_URL=http://grafana.riotpiao.homelab.com \
|
GRAFANA_URL=http://grafana.riotpiao.com \
|
||||||
MINIO_URL=http://minio.riotpiao.homelab.com \
|
MINIO_URL=http://minio.riotpiao.com \
|
||||||
python3 k8s/talos-iam/provision_oidc.py
|
python3 k8s/talos-iam/provision_oidc.py
|
||||||
|
|
||||||
# 2. Init Vault, wire JWT + OIDC auth, seed secrets
|
# 2. Init Vault, wire JWT + OIDC auth, seed secrets
|
||||||
@@ -474,7 +487,7 @@ Pods / Talos journal (both nodes)
|
|||||||
│
|
│
|
||||||
Grafana (logging ns) queries Loki + Prometheus via dashboards
|
Grafana (logging ns) queries Loki + Prometheus via dashboards
|
||||||
│
|
│
|
||||||
Nginx Ingress → grafana.riotpiao.homelab.com browser access
|
Nginx Ingress → grafana.riotpiao.com browser access
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example Applications & Workloads
|
## Example Applications & Workloads
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
## Cluster Architecture at a Glance
|
## Cluster Architecture at a Glance
|
||||||
|
|
||||||
**Homelab** is a 2-node bare-metal Kubernetes cluster deployed with Talos Linux, designed for self-hosted services, observability, and GitOps-ready CI/CD.
|
**Homelab** is a 3-node bare-metal Kubernetes cluster deployed with Talos Linux, designed for self-hosted services, observability, and GitOps-ready CI/CD.
|
||||||
|
|
||||||
|
### Node Topology (3 control-plane HA, since 2026-07-20)
|
||||||
|
|
||||||
|
| Node | IP | Zone | Role | Scheduling | Storage |
|
||||||
|
|------|----|----|------|-----------|---------|
|
||||||
|
| `talos-cp-1` | 192.168.1.213 | az-a | control-plane | **schedulable** (runs all workloads) | sole Longhorn node (sdb/sdc/sdd) |
|
||||||
|
| `talos-cp-2` | 192.168.1.163 | az-b | control-plane | dedicated (`NoSchedule`) | none |
|
||||||
|
| `talos-cp-3` | 192.168.1.166 | az-c | control-plane | dedicated (`NoSchedule`) | none |
|
||||||
|
|
||||||
|
- **etcd** has 3 voting members peering over the LAN (`cluster.etcd.advertisedSubnets: 192.168.1.0/24` — without it Talos may advertise on the WireGuard IP and new members hang as learners). Tolerates 1 node loss.
|
||||||
|
- Only `talos-cp-1` runs workloads and holds storage, so stateful services are **single-instance** (e.g. CNPG `ddb-cluster` = 1 instance). The kube-apiserver endpoint is single-homed to `.213` (no VIP yet).
|
||||||
|
- Scheduling is declarative: `allowSchedulingOnControlPlanes: true` + per-node `machine.nodeTaints` re-adds the control-plane taint on the dedicated nodes only.
|
||||||
|
|
||||||
### Deployment Stack (18 Helm releases)
|
### Deployment Stack (18 Helm releases)
|
||||||
|
|
||||||
@@ -12,7 +24,7 @@
|
|||||||
| **Certificates** | cert-manager + homelab-ca | cert-manager | Self-signed CA, auto-renewal |
|
| **Certificates** | cert-manager + homelab-ca | cert-manager | Self-signed CA, auto-renewal |
|
||||||
| **Storage (Block)** | Longhorn v1.7.0 | longhorn-system | Persistent volumes, default StorageClass |
|
| **Storage (Block)** | Longhorn v1.7.0 | longhorn-system | Persistent volumes, default StorageClass |
|
||||||
| **Storage (Object)** | MinIO (3-node, site-repl) | storage | S3-compatible, multi-AZ replication |
|
| **Storage (Object)** | MinIO (3-node, site-repl) | storage | S3-compatible, multi-AZ replication |
|
||||||
| **Database** | CloudNativePG (3 replicas) | ddb | PostgreSQL 16 + pgvector |
|
| **Database** | CloudNativePG (1 instance) | ddb | PostgreSQL 16 + pgvector (single-node; see topology) |
|
||||||
| **IAM / OIDC** | Authentik | iam | Federated OIDC provider for all services |
|
| **IAM / OIDC** | Authentik | iam | Federated OIDC provider for all services |
|
||||||
| **Secrets** | HashiCorp Vault | iam | KV secrets backend, JWT auth |
|
| **Secrets** | HashiCorp Vault | iam | KV secrets backend, JWT auth |
|
||||||
| **Logs** | Loki (SingleBinary) | logging | 10-day retention, MinIO backend |
|
| **Logs** | Loki (SingleBinary) | logging | 10-day retention, MinIO backend |
|
||||||
@@ -73,101 +85,252 @@ sqs (Kafka + Message Queue)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Custom CLI — `talos`
|
## Custom CLI — `core`
|
||||||
|
|
||||||
Homelab cluster control CLI (`core/`). Manages cluster nodes and Vault secrets.
|
Homelab cluster control CLI (source: `~/workplace/core/`). Manages cluster nodes, Authentik IAM, and Vault secrets.
|
||||||
|
|
||||||
### Secret path convention
|
**Setup:**
|
||||||
|
```bash
|
||||||
All secrets live under `cluster/<VARIABLE_NAME>`. The field name is always the variable name itself (SCREAMING_SNAKE_CASE), matching the `.env` key. Example paths:
|
make cli # builds + installs to ~/.local/bin/core
|
||||||
|
core auth login-oob # authenticate with Authentik (OOB flow)
|
||||||
```
|
core nodes # verify cluster access
|
||||||
cluster/ANTHROPIC_API_KEY
|
|
||||||
cluster/AUTHENTIK_FORGEJO_CLIENT_ID
|
|
||||||
cluster/AUTHENTIK_ARGOCD_CLIENT_SECRET
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### `talos put` — write a secret to Vault
|
**Full documentation:** See [~/workplace/core/USAGE.md](../core/USAGE.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
#### OAuth2 Out-of-Band (OOB) Login
|
||||||
|
|
||||||
|
Browser-based login with manual code entry (recommended).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
talos put cluster/VARIABLE_NAME VARIABLE_NAME="secret-value"
|
export CORE_CLI_SECRET="cyoVr96FB9oeq3o64DUG0BmoVzMPsOTIWxVEd8ZdTezKrEZYwrKpIRkOwrDQNEtF6QJyNUPH4mjr9jWokQY7KVBWX1KVUXFyyhgAHTqWRRWAYUJ8r3H35wLFiTfn5KsV"
|
||||||
talos put cluster/FORGEJO_ADMIN_PASSWORD FORGEJO_ADMIN_PASSWORD="$FORGEJO_ADMIN_PASSWORD"
|
core auth login-oob
|
||||||
```
|
```
|
||||||
|
|
||||||
Field name = variable name — never `value`.
|
Token expires in 8 hours. Check status:
|
||||||
|
```bash
|
||||||
|
core auth status # show expiry
|
||||||
|
core auth clear # force re-auth on next command
|
||||||
|
```
|
||||||
|
|
||||||
### `talos get` — fetch a secret from Vault
|
---
|
||||||
|
|
||||||
|
### Cluster Management
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
talos get cluster/VARIABLE_NAME --key VARIABLE_NAME # always specify --key
|
core nodes # list cluster nodes
|
||||||
talos get cluster/FORGEJO_ADMIN_PASSWORD --key FORGEJO_ADMIN_PASSWORD
|
core status <ip> # Talos node overview
|
||||||
talos get cluster/VARIABLE_NAME --json # full secret as JSON
|
core services <ip> # list Talos services
|
||||||
|
core logs <ip> # stream kubelet logs
|
||||||
|
core log-svc <ip> <svc> # logs for specific service (etcd, kubelet, etc.)
|
||||||
|
core pods clean # delete Failed/Evicted/Terminating pods
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: `talos get` uses `--key` (long flag), not a positional arg — unlike `talos secrets get`.
|
#### kubectl Context (LAN vs. WireGuard)
|
||||||
|
|
||||||
### `vsource` — load a `.env` into the shell
|
`cluster-config/kubeconfig` has two contexts pointed at the same cluster:
|
||||||
|
`admin@homelab-cluster` (LAN, `192.168.1.213:6443`) and `admin@homelab-cluster-1`
|
||||||
zsh function (lives in `~/.zshrc`, not in the repo — can reference but cannot run directly).
|
(WireGuard, `10.6.0.1:6443`). If `kubectl`/`core nodes` hangs or refuses the
|
||||||
Empty `.env` values are fetched from Vault at `cluster/<KEY>`; hardcoded values pass through.
|
connection, you're likely off-LAN — switch contexts:
|
||||||
|
|
||||||
```zsh
|
|
||||||
vsource # loads .env in current directory
|
|
||||||
vsource .env.local # loads a specific file
|
|
||||||
```
|
|
||||||
|
|
||||||
`.env` format — leave secrets empty, vsource resolves them from Vault:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ANTHROPIC_API_KEY= # fetched from cluster/ANTHROPIC_API_KEY
|
core config kube-list # list contexts
|
||||||
AUTHENTIK_ARGOCD_CLIENT_ID= # fetched from cluster/AUTHENTIK_ARGOCD_CLIENT_ID
|
core config kube-use admin@homelab-cluster-1 # switch to WireGuard path
|
||||||
DEBUG=true # hardcoded, passed through as-is
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Typical workflow for a generated secret
|
**Known gap:** `core config use <talos-context>` (the combined talosctl+kubectl
|
||||||
|
switch) only maps to `admin@homelab-cluster` today — its WireGuard mapping
|
||||||
|
(`home-cluster-wire-guard`) is stale, that kubectl context doesn't exist. Use
|
||||||
|
`core config kube-use admin@homelab-cluster-1` directly until that's fixed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Secret Management (Vault)
|
||||||
|
|
||||||
|
All secrets live under `cluster/<VARIABLE_NAME>`. Field name = variable name (SCREAMING_SNAKE_CASE).
|
||||||
|
|
||||||
|
#### Write Secret to Vault
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Store immediately after generation (keeps secrets out of shell history)
|
core put cluster/ANTHROPIC_API_KEY ANTHROPIC_API_KEY="sk-ant-..."
|
||||||
talos put cluster/AUTHENTIK_FORGEJO_CLIENT_SECRET AUTHENTIK_FORGEJO_CLIENT_SECRET="<paste>"
|
core put cluster/FORGEJO_ADMIN_PASSWORD FORGEJO_ADMIN_PASSWORD="secret123"
|
||||||
|
|
||||||
# 2. Use via subshell when creating K8s secrets
|
|
||||||
kubectl create secret generic my-secret \
|
|
||||||
--from-literal=client-secret="$(talos get cluster/AUTHENTIK_FORGEJO_CLIENT_SECRET --key AUTHENTIK_FORGEJO_CLIENT_SECRET)"
|
|
||||||
|
|
||||||
# 3. Or load into shell via vsource for helmfile/env-driven tools
|
|
||||||
vsource .env && helmfile apply
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### IAM Management (Federated OIDC, Phases 1–6 Complete)
|
**Key rule:** Field name must match variable name — never `value=`.
|
||||||
|
|
||||||
**Status:** ✅ Fully deployed (2026-07-02). Single federated OIDC provider (`talos-federation`) handles all service auth.
|
#### Fetch Secret from Vault
|
||||||
|
|
||||||
**Quick reference:**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# View roles and capabilities
|
core get cluster/ANTHROPIC_API_KEY # default field
|
||||||
talos iam roles list && talos iam roles describe admin
|
core get cluster/AUTHENTIK_ARGOCD_CLIENT_SECRET # full value
|
||||||
|
|
||||||
# Service registry (Grafana, MinIO, Forgejo, etc.)
|
|
||||||
talos iam services list && talos iam services describe grafana
|
|
||||||
|
|
||||||
# Agents (admin-bot, ci-bot with auto-rotation)
|
|
||||||
talos iam agents list && talos iam agents rotate ci-bot
|
|
||||||
|
|
||||||
# Role bindings (user → role with TTL)
|
|
||||||
talos iam bindings grant [email protected] devops --expires 2026-12-31
|
|
||||||
talos iam bindings list
|
|
||||||
|
|
||||||
# Audit trail (90-day retention, 12 event types)
|
|
||||||
talos iam audit list && talos iam audit export --format json
|
|
||||||
|
|
||||||
# OIDC provider sync with Authentik
|
|
||||||
talos iam providers sync-authentik
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**See `homelab/CLAUDE.md` § IAM Management for full reference** (roles, services, agents, bindings, audit, providers).
|
#### List All Secrets
|
||||||
|
|
||||||
**Vault paths:** All IAM state stored under `cluster/iam/{federation,roles,services,agents,bindings}`.
|
```bash
|
||||||
|
core secrets list
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Load into Shell (vsource)
|
||||||
|
|
||||||
|
zsh function in `~/.zshrc` — fetches empty `.env` values from Vault:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env format
|
||||||
|
ANTHROPIC_API_KEY= # fetched from Vault
|
||||||
|
AUTHENTIK_ARGOCD_CLIENT_ID= # fetched from Vault
|
||||||
|
DEBUG=true # hardcoded, passed through
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
vsource # loads .env in current dir
|
||||||
|
vsource .env.prod # loads specific file
|
||||||
|
eval "$(vsource .env)" && helmfile apply # inject + deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### IAM Management (Authentik)
|
||||||
|
|
||||||
|
Manage OAuth2 applications, groups, and user access via Authentik.
|
||||||
|
|
||||||
|
#### List Groups
|
||||||
|
|
||||||
|
```bash
|
||||||
|
core iam list-groups
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
authentik Admins (id: 9d72cbf2-9d52-4d3c-bba9-0068525d7a91)
|
||||||
|
grafana-admins (id: 5f2e1e79-7d7e-4ef0-9b99-3c6df19c0b88)
|
||||||
|
minio-admins (id: e640d887-eb85-431b-b5b2-5b6a8a7e0a44)
|
||||||
|
argocd-admins (id: 22a3c296-0d98-433f-8456-ebc2f1c8d489)
|
||||||
|
forgejo-admins (id: 4084c8d6-0c12-46af-acf8-7372172b9016)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### List OAuth2 Applications
|
||||||
|
|
||||||
|
```bash
|
||||||
|
core iam list-apps
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create New Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
core iam create-app "my-service" --slug my-service --redirect-uri "https://my-service.riotpiao.com/callback"
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns client ID and secret (save immediately).
|
||||||
|
|
||||||
|
#### Describe Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
core iam describe-app grafana
|
||||||
|
```
|
||||||
|
|
||||||
|
Shows:
|
||||||
|
- Client ID
|
||||||
|
- Client Secret
|
||||||
|
- Redirect URIs
|
||||||
|
- Scope claims
|
||||||
|
|
||||||
|
#### Bind Group to Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
core iam bind-app grafana grafana-admins
|
||||||
|
```
|
||||||
|
|
||||||
|
Members of `grafana-admins` can log in to Grafana via OIDC.
|
||||||
|
|
||||||
|
#### Create Group
|
||||||
|
|
||||||
|
```bash
|
||||||
|
core iam create-group "developers"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Add User to Group
|
||||||
|
|
||||||
|
```bash
|
||||||
|
core iam add-member grafana-admins newuser
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Rotate Application Secret
|
||||||
|
|
||||||
|
```bash
|
||||||
|
core iam rotate-secret grafana
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ Must update deployment after rotating.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Port Forwarding
|
||||||
|
|
||||||
|
```bash
|
||||||
|
core pf grafana # localhost:3000 → Grafana
|
||||||
|
core pf prometheus # localhost:9090 → Prometheus
|
||||||
|
core pf minio # localhost:9001 → MinIO console
|
||||||
|
core pf iam # localhost:7000 → Authentik
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Workflow: Rotate OAuth2 Secret
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Rotate in Authentik
|
||||||
|
SECRET=$(core iam rotate-secret grafana | jq -r '.client_secret')
|
||||||
|
|
||||||
|
# 2. Update deployment
|
||||||
|
vi k8s/logging/grafana-values.yaml
|
||||||
|
# Set: GRAFANA_OIDC_CLIENT_SECRET="$SECRET"
|
||||||
|
|
||||||
|
# 3. Redeploy
|
||||||
|
helmfile apply -l app=grafana
|
||||||
|
|
||||||
|
# 4. Verify
|
||||||
|
core iam describe-app grafana
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Workflow: Add User to Service
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create or verify group exists
|
||||||
|
core iam list-groups | grep minio-admins
|
||||||
|
|
||||||
|
# 2. Add user to group
|
||||||
|
core iam add-member minio-admins alice
|
||||||
|
|
||||||
|
# 3. Verify
|
||||||
|
# (User will have access next login via OIDC)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Vault Integration (Advanced)
|
||||||
|
|
||||||
|
Vault paths for IAM state (if using federated OIDC):
|
||||||
|
|
||||||
|
```
|
||||||
|
cluster/iam/federation/
|
||||||
|
cluster/iam/roles/
|
||||||
|
cluster/iam/services/
|
||||||
|
cluster/iam/agents/
|
||||||
|
cluster/iam/bindings/
|
||||||
|
```
|
||||||
|
|
||||||
|
Query via:
|
||||||
|
```bash
|
||||||
|
core get cluster/iam/roles/admin --key roles
|
||||||
|
```
|
||||||
|
|
||||||
|
**See `CLAUDE.md` § IAM for full architecture** (roles, services, agents, audit).
|
||||||
|
|
||||||
### CI/CD Image Registry Authentication (Forgejo + Runner)
|
### CI/CD Image Registry Authentication (Forgejo + Runner)
|
||||||
|
|
||||||
@@ -177,16 +340,16 @@ talos iam providers sync-authentik
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Get ci-bot JWT token (runner has this injected via ServiceAccount)
|
# 1. Get ci-bot JWT token (runner has this injected via ServiceAccount)
|
||||||
export REGISTRY_TOKEN=$(talos get cluster/iam/agents/ci-bot --key token)
|
export REGISTRY_TOKEN=$(core get cluster/iam/agents/ci-bot --key token)
|
||||||
|
|
||||||
# 2. Authenticate docker/podman to Forgejo registry
|
# 2. Authenticate docker/podman to Forgejo registry
|
||||||
docker login forgejo.riotpiao.homelab.com \
|
docker login forgejo.riotpiao.com \
|
||||||
--username ci-bot \
|
--username ci-bot \
|
||||||
--password "$REGISTRY_TOKEN"
|
--password "$REGISTRY_TOKEN"
|
||||||
|
|
||||||
# 3. Tag and push image
|
# 3. Tag and push image
|
||||||
docker tag myapp:latest forgejo.riotpiao.homelab.com/rock/myapp:latest
|
docker tag myapp:latest forgejo.riotpiao.com/rock/myapp:latest
|
||||||
docker push forgejo.riotpiao.homelab.com/rock/myapp:latest
|
docker push forgejo.riotpiao.com/rock/myapp:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
**Pull images in runner (automatic):**
|
**Pull images in runner (automatic):**
|
||||||
@@ -194,7 +357,7 @@ docker push forgejo.riotpiao.homelab.com/rock/myapp:latest
|
|||||||
```bash
|
```bash
|
||||||
# Inside .forgejo/workflows/*.yml, runner pulls via K8s ServiceAccount
|
# Inside .forgejo/workflows/*.yml, runner pulls via K8s ServiceAccount
|
||||||
# No explicit login needed — imagePullSecrets injected by runner pod
|
# No explicit login needed — imagePullSecrets injected by runner pod
|
||||||
image: forgejo.riotpiao.homelab.com/rock/myapp:latest
|
image: forgejo.riotpiao.com/rock/myapp:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
**Runner pod setup:**
|
**Runner pod setup:**
|
||||||
|
|||||||
@@ -1,324 +0,0 @@
|
|||||||
# Flux CD Integration Planning — START HERE
|
|
||||||
|
|
||||||
## What Just Happened?
|
|
||||||
|
|
||||||
Your subagent completed **comprehensive planning documentation** for integrating Flux CD v2 with your homelab's helmfile-based infrastructure.
|
|
||||||
|
|
||||||
**Three complete documents created:**
|
|
||||||
|
|
||||||
1. **FLUX_INTEGRATION_PLAN.md** (1,810 lines)
|
|
||||||
- Full technical specification with code examples
|
|
||||||
- Phase-by-phase implementation roadmap
|
|
||||||
- Conflict resolution & safety procedures
|
|
||||||
- Testing strategy & risk assessment
|
|
||||||
|
|
||||||
2. **FLUX_PLANNING_SUMMARY.md** (351 lines)
|
|
||||||
- Executive overview for stakeholders
|
|
||||||
- Decision matrices & quick reference
|
|
||||||
- Timeline & effort estimates
|
|
||||||
- Success metrics
|
|
||||||
|
|
||||||
3. **FLUX_PLANNING_INDEX.md** (356 lines)
|
|
||||||
- Navigation guide across all documents
|
|
||||||
- Quick start for different audiences
|
|
||||||
- FAQ & next steps
|
|
||||||
|
|
||||||
**Total:** 2,517 lines of planning documentation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The Plan in 60 Seconds
|
|
||||||
|
|
||||||
### What Problem Are We Solving?
|
|
||||||
|
|
||||||
Current helmfile workflow:
|
|
||||||
- Manual `helmfile apply` required
|
|
||||||
- No automatic drift detection
|
|
||||||
- No Git audit trail for changes
|
|
||||||
- No approval gates
|
|
||||||
- Hard to scale to multi-cluster
|
|
||||||
|
|
||||||
### What's the Solution?
|
|
||||||
|
|
||||||
Deploy **Flux CD v2** (GitOps) to:
|
|
||||||
- Continuously reconcile cluster state from Git
|
|
||||||
- Auto-detect & correct drift
|
|
||||||
- Maintain full audit trail
|
|
||||||
- Support staged rollouts with approval gates
|
|
||||||
- Keep helmfile.yaml.gotmpl as fallback during transition
|
|
||||||
|
|
||||||
### How Do We Do It?
|
|
||||||
|
|
||||||
**3 phases, 6–8 weeks, ~99 hours:**
|
|
||||||
|
|
||||||
| Phase | Timeline | Work | Goal |
|
|
||||||
|-------|----------|------|------|
|
|
||||||
| **1** | Weeks 1–2 | Bootstrap Flux + helmfile bridge | Zero breaking changes |
|
|
||||||
| **2** | Weeks 3–6 | Migrate 23 releases to HelmRelease CRDs | Parallel migration (4 streams) |
|
|
||||||
| **3** | Weeks 7–8 | Enable auto-sync, metrics, runbooks | Full GitOps readiness |
|
|
||||||
|
|
||||||
**Key:** No downtime. Helmfile stays functional as fallback throughout.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture Simplified
|
|
||||||
|
|
||||||
```
|
|
||||||
Git (Forgejo) ← Source of Truth
|
|
||||||
└─→ Flux Reconciliation Loop (every 5 min)
|
|
||||||
└─→ Kubernetes Cluster
|
|
||||||
└─→ 23 Helm Releases (reconciled state)
|
|
||||||
```
|
|
||||||
|
|
||||||
That's it. Flux watches Git. When you push changes, Flux applies them. If someone manually changes the cluster (kubectl), Flux auto-corrects on next reconciliation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Decisions (No Surprises)
|
|
||||||
|
|
||||||
| Decision | Choice | Reasoning |
|
|
||||||
|----------|--------|-----------|
|
|
||||||
| **Controller** | Flux v2 | Stable, battle-tested; v3 still beta |
|
|
||||||
| **Helm** | HelmRelease CRDs | Preserves values-based workflow |
|
|
||||||
| **Secrets** | SOPS + age | Git-stored, audited, simple |
|
|
||||||
| **Rollout** | Phased (3×8 weeks) | Lower risk, easier debugging |
|
|
||||||
|
|
||||||
All decisions explained in detail in FLUX_INTEGRATION_PLAN.md §3 (Architecture Decision Matrix).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What You Get
|
|
||||||
|
|
||||||
### By End of Phase 1 (Week 2)
|
|
||||||
- ✅ Flux running in cluster
|
|
||||||
- ✅ Git syncing every 60 seconds
|
|
||||||
- ✅ Helmfile still works as fallback
|
|
||||||
- ✅ Zero disruption to running workloads
|
|
||||||
|
|
||||||
### By End of Phase 2 (Week 6)
|
|
||||||
- ✅ All 23 releases migrated to Git-based HelmRelease CRDs
|
|
||||||
- ✅ Helmfile no longer used for deployments
|
|
||||||
- ✅ Every release tested & verified
|
|
||||||
- ✅ Full test suite in place
|
|
||||||
|
|
||||||
### By End of Phase 3 (Week 8)
|
|
||||||
- ✅ Automatic reconciliation enabled
|
|
||||||
- ✅ Drift detection + alerting working
|
|
||||||
- ✅ Metrics flowing to Prometheus
|
|
||||||
- ✅ Team trained on GitOps workflows
|
|
||||||
- ✅ RTO < 2 hours (restore from Git if needed)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How to Read the Documentation
|
|
||||||
|
|
||||||
### Quick Overview (10 min)
|
|
||||||
→ **Read:** FLUX_PLANNING_SUMMARY.md
|
|
||||||
|
|
||||||
Start here to understand what we're doing and why. Tables, diagrams, high-level summary. Perfect for stakeholder presentations.
|
|
||||||
|
|
||||||
### Getting Ready to Build (1 hour)
|
|
||||||
→ **Read:** FLUX_PLANNING_INDEX.md + FLUX_INTEGRATION_PLAN.md (Executive Summary)
|
|
||||||
|
|
||||||
Learn the full architecture, decision rationale, and how phases fit together.
|
|
||||||
|
|
||||||
### Phase 1 Implementation (Week 1–2)
|
|
||||||
→ **Reference:** FLUX_INTEGRATION_PLAN.md §5.1 (Phase 1: Flux Bootstrap)
|
|
||||||
|
|
||||||
Detailed tasks:
|
|
||||||
- 1.1: Bootstrap Flux into cluster
|
|
||||||
- 1.2: Create Git repo structure
|
|
||||||
- 1.3: HelmRepository CRDs (13 repos)
|
|
||||||
- 1.4: SOPS + age setup
|
|
||||||
- 1.5: Helmfile-bridge CronJob
|
|
||||||
|
|
||||||
### Phase 2 Migration (Weeks 3–6)
|
|
||||||
→ **Reference:** FLUX_INTEGRATION_PLAN.md §5.2 (Phase 2: HelmRelease Migration)
|
|
||||||
|
|
||||||
Four parallel streams:
|
|
||||||
- Stream A: Low-risk (reloader, prometheus)
|
|
||||||
- Stream B: Medium-risk (cert-manager, ingress)
|
|
||||||
- Stream C: High-risk secrets (authentik, vault)
|
|
||||||
- Stream D: Complex stateful (minio, forgejo)
|
|
||||||
|
|
||||||
Per-release process: generate CRD → validate → deploy → test → commit
|
|
||||||
|
|
||||||
### Phase 3 Production Readiness (Weeks 7–8)
|
|
||||||
→ **Reference:** FLUX_INTEGRATION_PLAN.md §5.3 (Phase 3: Continuous Reconciliation)
|
|
||||||
|
|
||||||
Auto-sync, metrics, runbooks, team training.
|
|
||||||
|
|
||||||
### Troubleshooting & Rollback
|
|
||||||
→ **Reference:** FLUX_INTEGRATION_PLAN.md §7 (Rollback & Safety Guardrails)
|
|
||||||
|
|
||||||
How to recover if something breaks:
|
|
||||||
- Suspend Flux + manual rollback
|
|
||||||
- Git revert + auto-reconciliation
|
|
||||||
- Disaster recovery from Git
|
|
||||||
|
|
||||||
### Testing Strategy
|
|
||||||
→ **Reference:** FLUX_INTEGRATION_PLAN.md §8 (Testing Strategy)
|
|
||||||
|
|
||||||
Unit tests, integration tests, chaos tests, production deployment strategy.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Risk Summary
|
|
||||||
|
|
||||||
### Main Risks & How We Handle Them
|
|
||||||
|
|
||||||
| Risk | Mitigation |
|
|
||||||
|------|-----------|
|
|
||||||
| **Flux + helmfile conflict** | Stagger reconciliation (helmfile 30min, Flux 5min) |
|
|
||||||
| **Secret injection breaks** | Three-tier approach (SOPS + ConfigMaps + .env fallback) |
|
|
||||||
| **Secrets leak in Git** | SOPS encryption from start + pre-commit hooks |
|
|
||||||
| **Cluster recovery fails** | Keep helmfile as fallback; test quarterly |
|
|
||||||
|
|
||||||
All risks detailed with specific mitigations in FLUX_INTEGRATION_PLAN.md §9 (Risk Assessment).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Timeline Reality Check
|
|
||||||
|
|
||||||
```
|
|
||||||
Week 1–2: Phase 1 bootstrap (20 hrs)
|
|
||||||
├─ 1 DevOps engineer + 1 Security engineer
|
|
||||||
└─ 0 downtime to running workloads
|
|
||||||
|
|
||||||
Week 3–6: Phase 2 migration (40 hrs)
|
|
||||||
├─ 4 parallel streams (DevOps + Ops + Security)
|
|
||||||
└─ Release-by-release (low risk)
|
|
||||||
|
|
||||||
Week 7–8: Phase 3 hardening (16 hrs)
|
|
||||||
├─ DevOps + QA
|
|
||||||
└─ Runbooks + training
|
|
||||||
|
|
||||||
Total: ~99 hours (~2.5 FTE-weeks)
|
|
||||||
6–8 calendar weeks (with parallelization)
|
|
||||||
```
|
|
||||||
|
|
||||||
Actual timeline depends on:
|
|
||||||
- Team size (4 engineers = 8 weeks; 2 engineers = 12 weeks)
|
|
||||||
- Experience with Flux (learning curve ~40 hours)
|
|
||||||
- Testing rigor (each phase adds 1–2 weeks)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Actions
|
|
||||||
|
|
||||||
### Immediately (Today)
|
|
||||||
|
|
||||||
1. **Review FLUX_PLANNING_SUMMARY.md** (15 min)
|
|
||||||
- Understand the approach
|
|
||||||
- Check decision matrix
|
|
||||||
- Confirm timeline is acceptable
|
|
||||||
|
|
||||||
2. **Share with stakeholders**
|
|
||||||
- Security team: review SOPS approach
|
|
||||||
- Ops team: review rollback procedures
|
|
||||||
- Management: confirm timeline & resources
|
|
||||||
|
|
||||||
3. **Get approval** for:
|
|
||||||
- Phased approach (6–8 weeks)
|
|
||||||
- Flux v2 + HelmRelease CRDs
|
|
||||||
- SOPS encryption for secrets
|
|
||||||
- ~99 hours effort
|
|
||||||
|
|
||||||
### Week 1 (Phase 1 Kickoff)
|
|
||||||
|
|
||||||
1. **Assign team members**
|
|
||||||
- DevOps lead
|
|
||||||
- Security engineer (SOPS)
|
|
||||||
- Ops engineer (testing)
|
|
||||||
|
|
||||||
2. **Bootstrap Flux**
|
|
||||||
- `flux bootstrap git` command
|
|
||||||
- Set up Git repo structure
|
|
||||||
- Deploy HelmRepository CRDs
|
|
||||||
|
|
||||||
3. **Start helmfile-bridge development**
|
|
||||||
- CronJob to run `helmfile apply` every 30 min
|
|
||||||
- Test alongside Flux (staggered intervals)
|
|
||||||
|
|
||||||
### Weeks 3–8 (Phases 2 & 3)
|
|
||||||
|
|
||||||
Follow the phase roadmap in FLUX_INTEGRATION_PLAN.md with weekly syncs.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files Created
|
|
||||||
|
|
||||||
All in `/Users/rockliang/workplace/homelab/`:
|
|
||||||
|
|
||||||
1. **FLUX_INTEGRATION_PLAN.md** (55 KB)
|
|
||||||
- Complete technical specification
|
|
||||||
- Phase-by-phase breakdown
|
|
||||||
- Code examples & detailed procedures
|
|
||||||
|
|
||||||
2. **FLUX_PLANNING_SUMMARY.md** (13 KB)
|
|
||||||
- Executive overview
|
|
||||||
- Decision matrices
|
|
||||||
- Quick reference tables
|
|
||||||
|
|
||||||
3. **FLUX_PLANNING_INDEX.md** (13 KB)
|
|
||||||
- Navigation guide
|
|
||||||
- Quick start by audience
|
|
||||||
- FAQ & related docs
|
|
||||||
|
|
||||||
4. **_FLUX_START_HERE.md** (this file)
|
|
||||||
- Quick orientation
|
|
||||||
- Next actions
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Questions to Ask
|
|
||||||
|
|
||||||
Before Phase 1 starts, clarify:
|
|
||||||
|
|
||||||
1. **Team capacity?** How many FTE can we dedicate?
|
|
||||||
- 4 FTE → 8 weeks
|
|
||||||
- 2 FTE → 12 weeks
|
|
||||||
|
|
||||||
2. **Timeline flexibility?** Hard deadline or can we adjust?
|
|
||||||
- If hard: compress with more parallel streams
|
|
||||||
- If flexible: add more testing/validation
|
|
||||||
|
|
||||||
3. **Flux experience on team?** Anyone used Flux before?
|
|
||||||
- If no: add 1–2 weeks for learning curve
|
|
||||||
- If yes: can reduce onboarding time
|
|
||||||
|
|
||||||
4. **Multi-cluster plans?** Will you add more clusters after homelab?
|
|
||||||
- If yes: design for portability from start
|
|
||||||
- If no: homelab-specific is fine
|
|
||||||
|
|
||||||
5. **SOPS comfort?** Any concerns about secret encryption in Git?
|
|
||||||
- If yes: alternative is store in Vault (referenced from HelmRelease)
|
|
||||||
- If no: SOPS is recommended
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Document Quality Checklist
|
|
||||||
|
|
||||||
The planning documentation includes:
|
|
||||||
|
|
||||||
- ✅ **Executive summary** — problem & solution in 1 page
|
|
||||||
- ✅ **Current state analysis** — what we're migrating from
|
|
||||||
- ✅ **Architecture decisions** — Flux v2, HelmRelease, SOPS (with reasoning)
|
|
||||||
- ✅ **Detailed design** — GitRepository, Kustomization, HelmRelease CRDs
|
|
||||||
- ✅ **3-phase roadmap** — specific tasks, timelines, deliverables, success criteria
|
|
||||||
- ✅ **Conflict resolution** — helmfile + Flux, .env → SOPS, kubectl drift
|
|
||||||
- ✅ **Rollback procedures** — what to do if something breaks
|
|
||||||
- ✅ **Safety guardrails** — RBAC, audit logging, validation webhooks, approval gates
|
|
||||||
- ✅ **Testing strategy** — unit, integration, chaos, production deployment
|
|
||||||
- ✅ **Risk assessment** — probability, impact, mitigation for each risk
|
|
||||||
- ✅ **Timeline & effort** — 99 hours, 6-8 weeks, team composition
|
|
||||||
- ✅ **Useful commands** — Flux CLI cheatsheet
|
|
||||||
- ✅ **FAQ** — downtime, rollback, recovery, cost
|
|
||||||
|
|
||||||
Ready for review and implementation kickoff.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Status:** Planning phase complete. Ready for team discussion & approval.
|
|
||||||
|
|
||||||
**Next:** Review FLUX_PLANNING_SUMMARY.md, approve approach, assign Phase 1 team.
|
|
||||||
Executable
+269
@@ -0,0 +1,269 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Phase-0 bootstrap — bring a bare Talos cluster to a self-hosted GitOps control
|
||||||
|
# plane, breaking the ArgoCD <-> Forgejo circle via a GitHub seed + cutover.
|
||||||
|
# See docs/adr/0001-gitops-bootstrap-and-cd.md (Part A) and docs/plans/0001-EXECUTION.md.
|
||||||
|
#
|
||||||
|
# Order (all manual, once): Cilium -> Longhorn -> CNPG operator -> forgejo-db
|
||||||
|
# (wait Ready) -> Forgejo -> ArgoCD (seeded from GitHub) -> cutover to Forgejo.
|
||||||
|
# Everything ELSE is deployed by ArgoCD from the seed repo, in sync-wave order.
|
||||||
|
#
|
||||||
|
# Prereqs:
|
||||||
|
# - Talos cluster up; kubectl context points at it
|
||||||
|
# - helm 3, kubectl
|
||||||
|
# - SOPS age key at $SOPS_KEY (for the ArgoCD SOPS CMP plugin)
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BOOT="$SCRIPT_DIR/k8s/bootstrap"
|
||||||
|
SOPS_KEY="${SOPS_KEY:-$HOME/.sops/key.txt}"
|
||||||
|
|
||||||
|
log() { echo "[$(date +%H:%M:%S)] $*"; }
|
||||||
|
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||||
|
phase(){ echo; echo "━━━ $* ━━━"; echo; }
|
||||||
|
|
||||||
|
# Idempotent helm repo setup
|
||||||
|
ensure_helm_repo() {
|
||||||
|
local name=$1 url=$2
|
||||||
|
helm repo list 2>/dev/null | grep -q "^$name" || helm repo add "$name" "$url" >/dev/null
|
||||||
|
helm repo update "$name" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
preflight() {
|
||||||
|
log "preflight…"
|
||||||
|
kubectl cluster-info >/dev/null || die "kubectl not configured / cluster unreachable"
|
||||||
|
command -v helm >/dev/null || die "helm 3 not found"
|
||||||
|
[[ -f "$SOPS_KEY" ]] || die "SOPS age key missing at $SOPS_KEY"
|
||||||
|
log "✅ preflight ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
p1_cilium() {
|
||||||
|
phase "PHASE 1a: CNI (Cilium)"
|
||||||
|
if kubectl -n kube-system get ds cilium >/dev/null 2>&1; then log "cilium present, skip"; return; fi
|
||||||
|
ensure_helm_repo cilium https://helm.cilium.io
|
||||||
|
helm install cilium cilium/cilium -n kube-system \
|
||||||
|
--set ipam.mode=kubernetes --set kubeProxyReplacement=true --wait --timeout 10m
|
||||||
|
log "✅ cilium installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
p1_longhorn() {
|
||||||
|
phase "PHASE 1b: STORAGE (Longhorn)"
|
||||||
|
|
||||||
|
# Always ensure namespace + StorageClasses (idempotent, resumable)
|
||||||
|
kubectl apply -f "$BOOT/phase1-storage/namespace.yaml"
|
||||||
|
|
||||||
|
# Install Longhorn if not present
|
||||||
|
if ! helm -n longhorn-system list 2>/dev/null | grep -q longhorn; then
|
||||||
|
ensure_helm_repo longhorn https://charts.longhorn.io
|
||||||
|
log "Installing Longhorn storage (this may take 5-10 minutes)..."
|
||||||
|
if helm install longhorn longhorn/longhorn -n longhorn-system \
|
||||||
|
--values "$BOOT/phase1-storage/longhorn-values.yaml" --wait --timeout 10m; then
|
||||||
|
log "✅ Longhorn installed"
|
||||||
|
else
|
||||||
|
log "⚠️ Helm install failed, but continuing to ensure resources..."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Always apply StorageClasses (even if helm install partially failed)
|
||||||
|
kubectl apply -f "$BOOT/phase1-storage/storageclasses.yaml"
|
||||||
|
|
||||||
|
# Verify critical components (resumable check)
|
||||||
|
if kubectl -n longhorn-system wait --for=condition=available --timeout=300s deploy/longhorn-manager 2>/dev/null; then
|
||||||
|
log "✅ longhorn installed"
|
||||||
|
else
|
||||||
|
log "⚠️ longhorn-manager not ready yet, but StorageClasses applied. Re-run to verify."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
p1_ingress() {
|
||||||
|
phase "PHASE 1c: INGRESS (Nginx Ingress Controller)"
|
||||||
|
|
||||||
|
# Install nginx-ingress if not present
|
||||||
|
if kubectl get ingressclass nginx >/dev/null 2>&1; then
|
||||||
|
log "nginx IngressClass present, skip install"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Always ensure namespace with PodSecurity labels (idempotent)
|
||||||
|
kubectl apply -f "$BOOT/ingress/namespace.yaml"
|
||||||
|
|
||||||
|
ensure_helm_repo ingress-nginx https://kubernetes.github.io/ingress-nginx
|
||||||
|
log "Installing nginx-ingress controller (this may take 2-3 minutes)..."
|
||||||
|
if helm install ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx \
|
||||||
|
--values "$BOOT/ingress/nginx-values.yaml" --timeout 5m; then
|
||||||
|
log "✅ nginx-ingress installed"
|
||||||
|
else
|
||||||
|
log "❌ nginx-ingress install failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Apply additional ingress resources (cert, ingress rules)
|
||||||
|
log "Applying ingress manifests (ignoring cert-manager CRD errors)..."
|
||||||
|
kubectl apply -k "$BOOT/ingress/" 2>&1 | grep -v "no matches for kind" || true
|
||||||
|
log "✅ Ingress resources applied (cert-manager resources will be created by ArgoCD)"
|
||||||
|
}
|
||||||
|
|
||||||
|
p2_cnpg() {
|
||||||
|
phase "PHASE 2: CNPG OPERATOR"
|
||||||
|
if kubectl get crd clusters.postgresql.cnpg.io >/dev/null 2>&1; then log "cnpg CRD present, skip install"; return; fi
|
||||||
|
ensure_helm_repo cnpg https://cloudnative-pg.github.io/charts
|
||||||
|
log "Installing CloudNativePG operator (this may take 2-3 minutes)..."
|
||||||
|
if helm install cnpg cnpg/cloudnative-pg -n cnpg-system --create-namespace \
|
||||||
|
--values "$BOOT/phase2-cnpg/cnpg-values.yaml" --wait --timeout 5m; then
|
||||||
|
log "✅ CNPG operator installed"
|
||||||
|
else
|
||||||
|
log "❌ CNPG operator install failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
kubectl get crd clusters.postgresql.cnpg.io >/dev/null || die "CNPG CRD not registered"
|
||||||
|
log "✅ cnpg operator installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
p3_forgejo() {
|
||||||
|
phase "PHASE 3: forgejo-db + Forgejo (ns cicd)"
|
||||||
|
|
||||||
|
# Always ensure namespace + NetworkPolicy + Secrets (idempotent)
|
||||||
|
kubectl apply -f "$BOOT/phase3-forgejo/namespace.yaml"
|
||||||
|
|
||||||
|
# Clean up old Valkey NetworkPolicy if it exists (from bundled chart)
|
||||||
|
kubectl delete networkpolicy forgejo-valkey-cluster -n cicd 2>/dev/null || true
|
||||||
|
|
||||||
|
# Apply CNPG-specific NetworkPolicy
|
||||||
|
kubectl apply -f "$BOOT/phase3-forgejo/cnpg-networkpolicy.yaml"
|
||||||
|
|
||||||
|
# Create Forgejo admin secret (bootstrap-time only, before ArgoCD exists)
|
||||||
|
# In GitOps mode, ArgoCD will sync the SOPS-encrypted version from git
|
||||||
|
if ! kubectl get secret forgejo-admin -n cicd >/dev/null 2>&1; then
|
||||||
|
log "Creating forgejo-admin secret from .env (bootstrap mode)"
|
||||||
|
[ -f "$HOME/workplace/homelab/.env" ] && source "$HOME/workplace/homelab/.env"
|
||||||
|
kubectl -n cicd create secret generic forgejo-admin \
|
||||||
|
--from-literal=username=rock \
|
||||||
|
--from-literal=password="${FORGEJO_ADMIN_PASSWORD}" \
|
||||||
|
--from-literal=email=[email protected]
|
||||||
|
else
|
||||||
|
log "forgejo-admin secret exists, skip (managed by ArgoCD in GitOps mode)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if forgejo-db cluster exists and is Ready
|
||||||
|
if kubectl get cluster forgejo-db -n cicd >/dev/null 2>&1; then
|
||||||
|
if kubectl get cluster forgejo-db -n cicd -o jsonpath='{.status.phase}' 2>/dev/null | grep -q "Cluster in healthy state"; then
|
||||||
|
log "forgejo-db already Ready, skip wait"
|
||||||
|
else
|
||||||
|
log "forgejo-db exists but not Ready, waiting for all 3 instances (up to 30 min)…"
|
||||||
|
if kubectl wait --for=condition=Ready --timeout=1800s cluster/forgejo-db -n cicd; then
|
||||||
|
log "✅ forgejo-db cluster is Ready"
|
||||||
|
else
|
||||||
|
log "❌ forgejo-db cluster failed to become Ready"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "creating forgejo-db cluster (3 instances)"
|
||||||
|
kubectl apply -f "$BOOT/phase3-forgejo/forgejo-db.yaml"
|
||||||
|
log "Waiting for all 3 CNPG instances to be Ready (up to 30 min)…"
|
||||||
|
if kubectl wait --for=condition=Ready --timeout=1800s cluster/forgejo-db -n cicd; then
|
||||||
|
log "✅ forgejo-db cluster is Ready"
|
||||||
|
else
|
||||||
|
log "❌ forgejo-db cluster failed to become Ready"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
kubectl -n cicd get secret forgejo-db-app >/dev/null || die "CNPG did not create forgejo-db-app secret"
|
||||||
|
|
||||||
|
# Install Forgejo if not present
|
||||||
|
if helm -n cicd list 2>/dev/null | grep -q forgejo; then log "forgejo helm release present, skip"; return; fi
|
||||||
|
ensure_helm_repo forgejo https://dl.gitea.io/charts/
|
||||||
|
log "Installing Forgejo (this may take 10-15 minutes on slow nodes)..."
|
||||||
|
if helm install forgejo forgejo/gitea -n cicd \
|
||||||
|
--values "$BOOT/phase3-forgejo/forgejo-values.yaml" --wait --timeout 10m; then
|
||||||
|
log "✅ Forgejo installed"
|
||||||
|
else
|
||||||
|
log "❌ Forgejo install failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
log "Forgejo is up — now push this repo to Forgejo and configure the GitHub pull-mirror"
|
||||||
|
}
|
||||||
|
|
||||||
|
p4_argocd() {
|
||||||
|
phase "PHASE 4: ArgoCD (seeded from GitHub)"
|
||||||
|
|
||||||
|
# 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 -
|
||||||
|
|
||||||
|
# Decrypt and apply any encrypted secrets from bootstrap dir (local SOPS)
|
||||||
|
if command -v sops &> /dev/null; then
|
||||||
|
export SOPS_AGE_KEY_FILE="$SOPS_KEY"
|
||||||
|
log "Decrypting encrypted secrets with local SOPS..."
|
||||||
|
local decrypted_count=0
|
||||||
|
for enc_file in "$BOOT"/phase*/**.enc.yaml; do
|
||||||
|
[ -f "$enc_file" ] || continue
|
||||||
|
log " → Decrypting $(basename "$enc_file")..."
|
||||||
|
if sops -d "$enc_file" | kubectl apply -f -; then
|
||||||
|
decrypted_count=$((decrypted_count + 1))
|
||||||
|
log " ✅ Applied"
|
||||||
|
else
|
||||||
|
log " ⚠️ Failed (may already exist)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
log "Decrypted and applied $decrypted_count secret(s)"
|
||||||
|
else
|
||||||
|
log "⚠️ SOPS not installed, skipping encrypted secret decryption"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install ArgoCD if not present
|
||||||
|
if ! helm -n argocd list 2>/dev/null | grep -q argocd; then
|
||||||
|
ensure_helm_repo argo https://argoproj.github.io/argo-helm
|
||||||
|
log "Installing ArgoCD via Helm (installing chart, pods will start afterward)..."
|
||||||
|
if helm install argocd argo/argo-cd -n argocd \
|
||||||
|
--values "$BOOT/phase4-argocd/argocd-values.yaml" --timeout 10m; then
|
||||||
|
log "✅ ArgoCD Helm release installed (pods starting...)"
|
||||||
|
else
|
||||||
|
log "❌ ArgoCD Helm install failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "ArgoCD Helm release already exists, skipping install"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wait for server ready (resumable - slow on talos-cp-2)
|
||||||
|
log "Waiting for argocd-server deployment to be available (max 10 minutes)..."
|
||||||
|
if kubectl -n argocd wait --for=condition=available --timeout=600s deploy/argocd-server; then
|
||||||
|
log "✅ argocd-server is available"
|
||||||
|
else
|
||||||
|
log "❌ argocd-server failed to become available within 10 minutes"
|
||||||
|
log "Check pods: kubectl get pods -n argocd"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Always apply root app (idempotent)
|
||||||
|
kubectl apply -f "$BOOT/phase4-argocd/root-app-github.yaml"
|
||||||
|
log "✅ ArgoCD syncing from GitHub seed. Watch: kubectl get applications -n argocd"
|
||||||
|
log "NOTE: SOPS CMP plugin not installed yet (bootstrap uses local SOPS decryption)."
|
||||||
|
log " To add SOPS plugin for GitOps, see k8s/bootstrap/phase4-argocd/argocd-cmp-cm.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
p5_cutover() {
|
||||||
|
phase "PHASE 5: CUTOVER GitHub -> Forgejo"
|
||||||
|
read -rp "Forgejo healthy AND mirroring GitHub? (y/N) " r; [[ $r =~ ^[Yy]$ ]] || die "push+mirror to Forgejo first"
|
||||||
|
kubectl apply -f "$BOOT/phase5-cutover/root-app-forgejo.yaml"
|
||||||
|
log "✅ root app now sourced from Forgejo. GitHub mirror = DR seed. Circle dead."
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-all}" in
|
||||||
|
all) preflight; p1_cilium; p1_longhorn; p1_ingress; p2_cnpg; p3_forgejo; p4_argocd
|
||||||
|
log "Phases 1-4 done. Push repo to Forgejo + set up pull-mirror, then: $0 cutover" ;;
|
||||||
|
cilium) preflight; p1_cilium ;;
|
||||||
|
storage) preflight; p1_longhorn ;;
|
||||||
|
ingress) preflight; p1_ingress ;;
|
||||||
|
cnpg) preflight; p2_cnpg ;;
|
||||||
|
forgejo) preflight; p3_forgejo ;;
|
||||||
|
argocd) preflight; p4_argocd ;;
|
||||||
|
cutover) preflight; p5_cutover ;;
|
||||||
|
*) echo "usage: $0 {all|cilium|storage|ingress|cnpg|forgejo|argocd|cutover}"; exit 1 ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# cluster-config/cilium-values.yaml
|
||||||
|
# Cilium CNI — installed via talosctl (not helmfile) during cluster bootstrap.
|
||||||
|
# Applied once: `helm install cilium cilium/cilium -n kube-system -f cilium-values.yaml`
|
||||||
|
#
|
||||||
|
# Why Cilium: Talos Linux does not ship kube-proxy. Cilium's eBPF dataplane
|
||||||
|
# replaces it entirely (kubeProxyReplacement: true) and also handles LB-IPAM
|
||||||
|
# so LoadBalancer services get real IPs without MetalLB.
|
||||||
|
|
||||||
|
# ── cgroup ────────────────────────────────────────────────────────────────────
|
||||||
|
# Talos mounts cgroups at boot before any container runtime starts.
|
||||||
|
# autoMount: false tells Cilium to use the existing mount rather than trying
|
||||||
|
# to mount its own — double-mounting on Talos causes init failures.
|
||||||
|
cgroup:
|
||||||
|
autoMount:
|
||||||
|
enabled: false
|
||||||
|
hostRoot: /sys/fs/cgroup # where Talos exposes the cgroup v2 hierarchy
|
||||||
|
|
||||||
|
# ── IPAM ──────────────────────────────────────────────────────────────────────
|
||||||
|
# kubernetes mode: Cilium allocates pod IPs from the pod CIDR that Talos
|
||||||
|
# configured for each node (--pod-cidr in the kubelet). Alternative is
|
||||||
|
# Cilium's own cluster-pool IPAM, but that requires extra config and
|
||||||
|
# conflicts with the Talos node CIDR assignment.
|
||||||
|
ipam:
|
||||||
|
mode: kubernetes
|
||||||
|
|
||||||
|
# ── Operator ──────────────────────────────────────────────────────────────────
|
||||||
|
# Single replica is fine for a 3-node homelab. The operator manages CiliumNode
|
||||||
|
# objects and LB-IPAM pools — it does not sit in the data path.
|
||||||
|
operator:
|
||||||
|
replicas: 1
|
||||||
|
|
||||||
|
# ── kube-proxy replacement ────────────────────────────────────────────────────
|
||||||
|
# Talos is deliberately installed without kube-proxy (machineConfig
|
||||||
|
# install.extensions excludes it). Cilium must replace it completely —
|
||||||
|
# partial replacement would leave Service ClusterIPs unreachable.
|
||||||
|
kubeProxyReplacement: true
|
||||||
|
|
||||||
|
# ── L2 announcements ──────────────────────────────────────────────────────────
|
||||||
|
# Without this, LB-IPAM (k8s/cilium/lb-ipam-pool.yaml) assigns real IPs to
|
||||||
|
# LoadBalancer Services, but nothing ARPs for them on the LAN — the IP shows
|
||||||
|
# up in `kubectl get svc` but is 100% unreachable from outside the cluster
|
||||||
|
# (confirmed: forgejo's .165 and shadowsocks' .166 both had incomplete ARP
|
||||||
|
# entries and 100% ping loss before this). This flag is what actually makes
|
||||||
|
# k8s/cilium/l2-announcement-policy.yaml take effect instead of being inert.
|
||||||
|
l2announcements:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# ── API server endpoint ───────────────────────────────────────────────────────
|
||||||
|
# Cilium needs to talk to the Kubernetes API to watch Nodes/Services/Endpoints.
|
||||||
|
# On Talos the API server listens on 127.0.0.1:7445 locally (the external
|
||||||
|
# port 6443 requires the node's external cert, which may not be available
|
||||||
|
# during early bootstrap). This is the standard Talos Cilium bootstrap config.
|
||||||
|
k8sServiceHost: 127.0.0.1
|
||||||
|
k8sServicePort: 7445
|
||||||
|
|
||||||
|
# ── Security context / capabilities ──────────────────────────────────────────
|
||||||
|
# Cilium's eBPF programs run in the kernel and require elevated capabilities.
|
||||||
|
# These are the minimum set needed — removing any of them breaks networking.
|
||||||
|
#
|
||||||
|
# NET_ADMIN / NET_RAW — manipulate iptables/nftables and raw sockets
|
||||||
|
# IPC_LOCK — lock eBPF maps in memory (prevents paging out BPF state)
|
||||||
|
# SYS_ADMIN — call bpf() syscall and mount BPF filesystem
|
||||||
|
# SYS_RESOURCE — raise RLIMIT_MEMLOCK for BPF map memory
|
||||||
|
# DAC_OVERRIDE / FOWNER / SETGID / SETUID — file permission ops during init
|
||||||
|
# CHOWN / KILL — container lifecycle management
|
||||||
|
#
|
||||||
|
# cleanCiliumState runs as a one-shot init container to wipe stale eBPF state
|
||||||
|
# on upgrades — it needs NET_ADMIN, SYS_ADMIN, SYS_RESOURCE only.
|
||||||
|
securityContext:
|
||||||
|
capabilities:
|
||||||
|
ciliumAgent:
|
||||||
|
- CHOWN
|
||||||
|
- KILL
|
||||||
|
- NET_ADMIN
|
||||||
|
- NET_RAW
|
||||||
|
- IPC_LOCK
|
||||||
|
- SYS_ADMIN
|
||||||
|
- SYS_RESOURCE
|
||||||
|
- DAC_OVERRIDE
|
||||||
|
- FOWNER
|
||||||
|
- SETGID
|
||||||
|
- SETUID
|
||||||
|
cleanCiliumState:
|
||||||
|
- NET_ADMIN
|
||||||
|
- SYS_ADMIN
|
||||||
|
- SYS_RESOURCE
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# bootstrap.sh
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Wait for cluster to be ready
|
||||||
|
kubectl wait --for=condition=Ready nodes --all --timeout=300s
|
||||||
|
|
||||||
|
# Longhorn requires privileged pods and hostPath volumes
|
||||||
|
kubectl create namespace longhorn-system --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
kubectl label namespace longhorn-system \
|
||||||
|
pod-security.kubernetes.io/enforce=privileged \
|
||||||
|
pod-security.kubernetes.io/enforce-version=latest \
|
||||||
|
--overwrite
|
||||||
|
|
||||||
|
# Install Longhorn
|
||||||
|
kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.7.0/deploy/longhorn.yaml
|
||||||
|
|
||||||
|
# Set as default StorageClass
|
||||||
|
kubectl patch storageclass longhorn \
|
||||||
|
-p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Trust the homelab-ca CA for pulling from the Forgejo container registry.
|
||||||
|
# Without this, containerd fails: x509 certificate signed by unknown authority
|
||||||
|
# (nginx terminates forgejo.riotpiao.com TLS with a homelab-ca cert).
|
||||||
|
# Apply: talosctl -n <node> patch mc --patch @cluster-config/patches/forgejo-registry-ca.yaml
|
||||||
|
machine:
|
||||||
|
registries:
|
||||||
|
config:
|
||||||
|
forgejo.riotpiao.com:
|
||||||
|
tls:
|
||||||
|
ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJiVENDQVJTZ0F3SUJBZ0lVYTBVaGs3Rm81d3BiWjZsRzVEWWJUVkFic1k4d0NnWUlLb1pJemowRUF3SXcKRlRFVE1CRUdBMVVFQXhNS2FHOXRaV3hoWWkxallUQWVGdzB5TmpBM01UQXdORFF3TXpaYUZ3MHpOakEzTURjdwpORFF3TXpaYU1CVXhFekFSQmdOVkJBTVRDbWh2YldWc1lXSXRZMkV3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPClBRTUJCd05DQUFTc1pNU2piUWI0YzNiUk00MjMxVEVrRXVLTnFLUUhaaW5uYnUzbWZGbStRc0wweTF3cjg1Uk0KUWJ6ZEZ2N01JZmN4REpMbHFqQTY1bEJ6TE9pdHRZZHRvMEl3UURBT0JnTlZIUThCQWY4RUJBTUNBcVF3RHdZRApWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVUVmcGJQL3FnYWsxaXUvQzdaQi9uZk5zc0hpd3dDZ1lJCktvWkl6ajBFQXdJRFJ3QXdSQUlnR2ltdnJiWU1xZjhGYThCeTBBM0M1ak1VL0d3dGU0NHgzOU4rRDRyaTJ1a0MKSUNkOEtIQXhhV0s2ZkVJcEFYZGdUQ1FxQmFiZjVZUDdhQzNDVzkzYkNsTjIKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# helmfile.yaml — DEPRECATED
|
||||||
|
#
|
||||||
|
# This file is kept for historical reference only.
|
||||||
|
# All Helm releases have been migrated to:
|
||||||
|
# 1. Terraform (bootstrap releases: cert-manager, reloader, ingress-nginx)
|
||||||
|
# 2. ArgoCD Applications (all workload releases)
|
||||||
|
#
|
||||||
|
# Deployment now uses:
|
||||||
|
# - terraform apply (for core infrastructure)
|
||||||
|
# - ArgoCD auto-sync (for all workloads)
|
||||||
|
#
|
||||||
|
# To view or manage releases:
|
||||||
|
# kubectl get applications -n argocd
|
||||||
|
#
|
||||||
|
# To modify releases, update k8s/argocd/apps/*.yaml files and commit to git.
|
||||||
|
#
|
||||||
|
# This file remains as a marker to prevent accidental `helmfile apply` usage.
|
||||||
|
# DELETE if no longer needed after full migration verification.
|
||||||
|
|
||||||
|
# Historical note:
|
||||||
|
# - Phases 0-3 migrated: cert-manager, reloader, ingress-nginx, strimzi-operator,
|
||||||
|
# kmsvc-redis, kafka-cluster, queue-crd, management-service, promtail, portainer,
|
||||||
|
# cloudnative-pg, loki, grafana, prometheus, forgejo, forgejo-runner, authentik
|
||||||
|
# - Phase 4 cutover: 2026-07-15 (helmfile stubbed, ArgoCD becomes sole convergence engine)
|
||||||
|
|
||||||
|
# ── DO NOT USE ────────────────────────────────────────────────────────────────
|
||||||
|
# helmfile apply # FORBIDDEN (use ArgoCD)
|
||||||
|
# helmfile diff # FORBIDDEN (use argocd app diff)
|
||||||
|
# helmfile destroy # FORBIDDEN (use kubectl delete)
|
||||||
|
|
||||||
|
# For drift detection (CI only):
|
||||||
|
# argocd app diff <app-name> # Check what ArgoCD would change
|
||||||
|
# terraform plan # Check what Terraform would change
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
data:
|
||||||
|
settings.json: |
|
||||||
|
{
|
||||||
|
"defaultProvider": "homelab-ornith",
|
||||||
|
"defaultModel": "ornith:35b",
|
||||||
|
"defaultThinkingLevel": "medium",
|
||||||
|
"theme": "light",
|
||||||
|
"compaction": {
|
||||||
|
"enabled": true,
|
||||||
|
"reserveTokens": 8192,
|
||||||
|
"keepRecentTokens": 12000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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,108 @@
|
|||||||
|
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.
|
||||||
|
- name: pi
|
||||||
|
image: node:22-slim
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
apt-get update && apt-get install -y git curl jq openssh-client tmux
|
||||||
|
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
|
||||||
|
node /root/hub.js
|
||||||
|
env:
|
||||||
|
- name: PI_BIN
|
||||||
|
value: pi
|
||||||
|
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: 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
|
||||||
|
- key: brave-search-SKILL.md
|
||||||
|
path: brave-search/SKILL.md
|
||||||
|
- name: hub-src
|
||||||
|
configMap:
|
||||||
|
name: hub-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,11 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: agent-pod
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- configmap.yaml
|
||||||
|
- hub-configmap.yaml
|
||||||
|
- pi-skills-configmap.yaml
|
||||||
|
- ssh-configmap.yaml
|
||||||
|
- hub-service.yaml
|
||||||
|
- console-ingress.yaml
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
data:
|
||||||
|
brave-search-SKILL.md: |
|
||||||
|
---
|
||||||
|
name: brave-search
|
||||||
|
description: Web search via the Brave Search API, called directly with curl. Use for searching documentation, facts, or any current web content.
|
||||||
|
allowed-tools: Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
# Brave Search
|
||||||
|
|
||||||
|
Direct HTTP call to the Brave Search API — no separate script or package, just `curl` (`BRAVE_API_KEY` is already set in the environment).
|
||||||
|
|
||||||
|
## Search
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "Accept: application/json" -H "X-Subscription-Token: $BRAVE_API_KEY" \
|
||||||
|
--get --data-urlencode "q=<query>" --data-urlencode "count=5" \
|
||||||
|
"https://api.search.brave.com/res/v1/web/search"
|
||||||
|
```
|
||||||
|
|
||||||
|
Options (add as extra `--data-urlencode` pairs):
|
||||||
|
- `count=<n>` — number of results (max 20, default 5)
|
||||||
|
- `country=<code>` — two-letter country code (default US)
|
||||||
|
- `freshness=pd|pw|pm|py` — past day/week/month/year, or `freshness=YYYY-MM-DDtoYYYY-MM-DD`
|
||||||
|
|
||||||
|
Response is JSON; the results live at `.web.results[]`, each with `title`, `url`, `description`, `age`. Pipe through `jq` if you want a shorter view, e.g.:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "Accept: application/json" -H "X-Subscription-Token: $BRAVE_API_KEY" \
|
||||||
|
--get --data-urlencode "q=<query>" "https://api.search.brave.com/res/v1/web/search" \
|
||||||
|
| jq -r '.web.results[] | "- \(.title)\n \(.url)\n \(.description)\n"'
|
||||||
|
```
|
||||||
|
|
||||||
|
There's no page-content-extraction helper here — if a result needs reading in full, `curl` the URL directly and read the raw HTML/text; don't expect readability-cleaned markdown.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Searching for documentation or API references
|
||||||
|
- Looking up facts or current information
|
||||||
|
- Confirming a claim or approach against real sources
|
||||||
|
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.
|
||||||
|
|
||||||
|
**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) — use the `brave-search` skill (`curl` against the Brave Search API) 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), use `brave-search` 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?
|
||||||
|
- 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,317 @@
|
|||||||
|
# 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
|
||||||
|
# ornith:35b -> ornith-predictor Ollama
|
||||||
|
# qwen2.5:3b-instruct -> ornith-predictor Ollama (same pod!)
|
||||||
|
# nomic-embed-text-v2 -> embeddings-predictor TEI
|
||||||
|
# bge-reranker-base -> reranker-predictor TEI
|
||||||
|
# Qwen2.5-Math-PRM-7B -> verifier-predictor vLLM pooling
|
||||||
|
#
|
||||||
|
# ── 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. But `ornith:35b` and `qwen2.5:3b-instruct`
|
||||||
|
# share ONE Ollama pod, and Ollama still reads which model to load from the
|
||||||
|
# body's `model` field. If only the path selected the route, a client calling
|
||||||
|
# /v1/qwen/... with `"model": "ornith:35b"` in the body would silently get the
|
||||||
|
# 35B model. So each chat route force-overwrites `model` in the body, making the
|
||||||
|
# path the single source of truth. 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},
|
||||||
|
{"id":"Qwen/Qwen2.5-Math-PRM-7B","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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
---
|
||||||
|
# ── POST /v1/score ──────────────────────────────────────────────────────────
|
||||||
|
# The process reward model. Returns scores, not tokens, so it is deliberately
|
||||||
|
# not under /chat/completions. vLLM serves /v1/score natively (verified), so no
|
||||||
|
# rewrite is needed.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: llm-score
|
||||||
|
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/score
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: verifier-predictor
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# API auth layer — Kong key-auth on the model routes.
|
||||||
|
#
|
||||||
|
# The model API (api.riotpiao.com/v1/...) requires a static API key, presented
|
||||||
|
# OpenAI-style as `Authorization: Bearer <key>` (or `apikey: <key>`). The key
|
||||||
|
# lives in the ksops-managed Secret model-invoke-apikey (labelled
|
||||||
|
# konghq.com/credential: key-auth) and is bound to the KongConsumer below.
|
||||||
|
#
|
||||||
|
# Issue the key to rock; use it as the OpenAI SDK api_key. Rotate by updating the
|
||||||
|
# ksops secret. This is self-contained in Kong — the invoke path does not depend
|
||||||
|
# on an Authentik token (Authentik still fronts every *human* dashboard SSO).
|
||||||
|
---
|
||||||
|
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
|
||||||
|
---
|
||||||
|
# key-auth: require the API key on the model routes. key_in_header accepts the
|
||||||
|
# `apikey` header; key_in_bearer accepts `Authorization: Bearer <key>` so any
|
||||||
|
# OpenAI-compatible SDK (api_key=..., base_url=https://api.riotpiao.com/v1) works
|
||||||
|
# unchanged.
|
||||||
|
#
|
||||||
|
# Namespace `llm-serving`, not `api`: the ingress controller resolves a
|
||||||
|
# `konghq.com/plugins` annotation against the annotated object's OWN namespace,
|
||||||
|
# and all five model routes in llm-routes.yaml live in llm-serving. While this
|
||||||
|
# sat in `api` the reference dangled, the plugin never bound, and every model
|
||||||
|
# route served traffic with no key at all — verified: an unauthenticated
|
||||||
|
# /v1/models and /v1/ornith/chat/completions both returned 200. A dangling
|
||||||
|
# plugin reference is silent; it fails open, so re-test without a key after any
|
||||||
|
# move rather than trusting that the object exists.
|
||||||
|
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
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: cloudflared
|
||||||
|
namespace: cloudflared
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: cloudflared
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: cloudflared
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: cloudflared
|
||||||
|
image: cloudflare/cloudflared:latest
|
||||||
|
args:
|
||||||
|
- tunnel
|
||||||
|
- --no-autoupdate
|
||||||
|
- run
|
||||||
|
- --token
|
||||||
|
- $(TUNNEL_TOKEN)
|
||||||
|
env:
|
||||||
|
- name: TUNNEL_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: cloudflared-token
|
||||||
|
key: token
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 32Mi
|
||||||
|
limits:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: cloudflared
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Homarr landing page with Authentik SSO
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
repository: ghcr.io/homarr-labs/homarr
|
||||||
|
tag: "latest"
|
||||||
|
pullPolicy: Always
|
||||||
|
|
||||||
|
replicaCount: 1
|
||||||
|
|
||||||
|
# Configure SSO via environment variables
|
||||||
|
# Chart supports these via top-level env dict (not array)
|
||||||
|
env:
|
||||||
|
AUTH_PROVIDERS: "oidc,credentials"
|
||||||
|
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_GROUPS_ATTRIBUTE: "groups"
|
||||||
|
AUTH_OIDC_SCOPE_OVERWRITE: "openid email profile groups"
|
||||||
|
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"
|
||||||
|
NEXTAUTH_URL: "https://homarr.riotpiao.com"
|
||||||
|
|
||||||
|
# Client credentials from homarr-oidc secret
|
||||||
|
# Chart doesn't support envFrom, so we add via extraEnv
|
||||||
|
extraEnv:
|
||||||
|
- name: AUTH_OIDC_CLIENT_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: homarr-oidc
|
||||||
|
key: client-id
|
||||||
|
- name: AUTH_OIDC_CLIENT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: homarr-oidc
|
||||||
|
key: client-secret
|
||||||
|
|
||||||
|
tolerations:
|
||||||
|
- key: node-role.kubernetes.io/control-plane
|
||||||
|
operator: Exists
|
||||||
|
effect: NoSchedule
|
||||||
|
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 384Mi
|
||||||
|
limits:
|
||||||
|
# Next.js 16 + bundled redis + the icon-updater (28k icons) saturated the old
|
||||||
|
# 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
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: dashboard
|
||||||
|
# Probes are now tuned via homarr-values.yaml (chart-native); the old
|
||||||
|
# fix-probes-job PostSync hook is removed. homarr-secrets/auth-oidc/db-encryption
|
||||||
|
# Secrets are delivered by the sops-secrets (ksops) 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,19 @@
|
|||||||
|
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
|
||||||
|
- verifier.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,108 @@
|
|||||||
|
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
|
||||||
|
maxReplicas: 1
|
||||||
|
minReplicas: 1
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: worker-1
|
||||||
|
runtimeClassName: nvidia
|
||||||
|
volumes:
|
||||||
|
- name: models
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: llm-models
|
||||||
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
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:
|
||||||
|
- --model=unsloth/DeepSeek-R1-Distill-Qwen-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
|
||||||
|
- --reasoning-parser=deepseek_r1
|
||||||
|
- --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: 16Gi
|
||||||
|
nvidia.com/gpu: '1'
|
||||||
|
requests:
|
||||||
|
cpu: '8'
|
||||||
|
memory: 8Gi
|
||||||
|
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
|
||||||
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
apiVersion: serving.kserve.io/v1beta1
|
||||||
|
kind: InferenceService
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
serving.kserve.io/deploymentMode: RawDeployment
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: llm-verifier
|
||||||
|
app.kubernetes.io/part-of: llm-serving
|
||||||
|
name: verifier
|
||||||
|
namespace: llm-serving
|
||||||
|
spec:
|
||||||
|
predictor:
|
||||||
|
containers:
|
||||||
|
- args:
|
||||||
|
- --model=Qwen/Qwen2.5-Math-PRM-7B
|
||||||
|
- --served-model-name=verifier
|
||||||
|
- --runner=pooling
|
||||||
|
- --dtype=float16
|
||||||
|
- --tensor-parallel-size=1
|
||||||
|
- --max-model-len=4096
|
||||||
|
- --max-num-seqs=8
|
||||||
|
- --host=0.0.0.0
|
||||||
|
- --port=8080
|
||||||
|
env:
|
||||||
|
- name: VLLM_USE_FLASHINFER_SAMPLER
|
||||||
|
value: '0'
|
||||||
|
- name: VLLM_ATTENTION_BACKEND
|
||||||
|
value: XFORMERS
|
||||||
|
- 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: 16Gi
|
||||||
|
nvidia.com/gpu: '1'
|
||||||
|
requests:
|
||||||
|
cpu: '4'
|
||||||
|
memory: 8Gi
|
||||||
|
nvidia.com/gpu: '1'
|
||||||
|
startupProbe:
|
||||||
|
failureThreshold: 60
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
periodSeconds: 15
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /mnt/models
|
||||||
|
name: models
|
||||||
|
- mountPath: /dev/shm
|
||||||
|
name: shm
|
||||||
|
deploymentStrategy:
|
||||||
|
type: Recreate
|
||||||
|
maxReplicas: 1
|
||||||
|
minReplicas: 1
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: worker-1
|
||||||
|
runtimeClassName: nvidia
|
||||||
|
volumes:
|
||||||
|
- name: models
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: llm-models
|
||||||
|
- emptyDir:
|
||||||
|
medium: Memory
|
||||||
|
sizeLimit: 1Gi
|
||||||
|
name: shm
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: kafka-cluster
|
||||||
|
description: Strimzi Kafka/KafkaNodePool CRs for the kmsvc Kafka cluster (design.md §7)
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
apiVersion: kafka.strimzi.io/v1beta2
|
||||||
|
kind: Kafka
|
||||||
|
metadata:
|
||||||
|
name: {{ .Values.clusterName }}
|
||||||
|
namespace: {{ .Values.namespace }}
|
||||||
|
annotations:
|
||||||
|
strimzi.io/node-pools: enabled
|
||||||
|
strimzi.io/kraft: enabled
|
||||||
|
spec:
|
||||||
|
kafka:
|
||||||
|
version: 4.0.0
|
||||||
|
metadataVersion: 4.0-IV3
|
||||||
|
listeners:
|
||||||
|
- name: plain
|
||||||
|
port: 9092
|
||||||
|
type: internal
|
||||||
|
tls: false
|
||||||
|
- name: tls
|
||||||
|
port: 9093
|
||||||
|
type: internal
|
||||||
|
tls: true
|
||||||
|
config:
|
||||||
|
default.replication.factor: {{ .Values.kafka.replicationFactor }}
|
||||||
|
min.insync.replicas: {{ .Values.kafka.minInsyncReplicas }}
|
||||||
|
offsets.topic.replication.factor: {{ .Values.kafka.replicationFactor }}
|
||||||
|
transaction.state.log.replication.factor: {{ .Values.kafka.replicationFactor }}
|
||||||
|
transaction.state.log.min.isr: {{ .Values.kafka.minInsyncReplicas }}
|
||||||
|
entityOperator:
|
||||||
|
topicOperator: {}
|
||||||
|
userOperator: {}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
apiVersion: kafka.strimzi.io/v1beta2
|
||||||
|
kind: KafkaNodePool
|
||||||
|
metadata:
|
||||||
|
name: {{ .Values.clusterName }}-pool
|
||||||
|
namespace: {{ .Values.namespace }}
|
||||||
|
labels:
|
||||||
|
strimzi.io/cluster: {{ .Values.clusterName }}
|
||||||
|
spec:
|
||||||
|
replicas: {{ .Values.nodePool.replicas }}
|
||||||
|
roles:
|
||||||
|
- controller
|
||||||
|
- broker
|
||||||
|
storage:
|
||||||
|
type: persistent-claim
|
||||||
|
size: {{ .Values.nodePool.storage.sizeGi }}Gi
|
||||||
|
class: {{ .Values.nodePool.storage.class }}
|
||||||
|
deleteClaim: false
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: {{ .Values.nodePool.resources.memory }}
|
||||||
|
cpu: {{ .Values.nodePool.resources.cpu | quote }}
|
||||||
|
requests:
|
||||||
|
memory: {{ .Values.nodePool.resources.memory }}
|
||||||
|
cpu: {{ .Values.nodePool.resources.cpu | quote }}
|
||||||
|
template:
|
||||||
|
pod:
|
||||||
|
affinity:
|
||||||
|
podAntiAffinity:
|
||||||
|
preferredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
- weight: 100
|
||||||
|
podAffinityTerm:
|
||||||
|
topologyKey: {{ .Values.nodePool.antiAffinityTopologyKey }}
|
||||||
|
labelSelector:
|
||||||
|
matchLabels:
|
||||||
|
strimzi.io/cluster: {{ .Values.clusterName }}
|
||||||
|
kafkaContainer:
|
||||||
|
env:
|
||||||
|
- name: KAFKA_HEAP_OPTS
|
||||||
|
value: {{ .Values.nodePool.heapOpts | quote }}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
clusterName: kmsvc
|
||||||
|
namespace: sqs
|
||||||
|
|
||||||
|
nodePool:
|
||||||
|
replicas: 3
|
||||||
|
storage:
|
||||||
|
class: longhorn
|
||||||
|
# Longhorn's per-node scheduling budget on the current 2-node cluster has
|
||||||
|
# only ~36Gi of headroom left (other PVCs already reserve the rest), and
|
||||||
|
# each node hosts one replica of all 3 broker volumes -- so 3 * sizeGi
|
||||||
|
# must fit in that headroom. Revisit once the 3rd node joins.
|
||||||
|
sizeGi: 10
|
||||||
|
resources:
|
||||||
|
memory: 5Gi
|
||||||
|
cpu: "2"
|
||||||
|
heapOpts: "-Xms2g -Xmx2g"
|
||||||
|
# design.md §7: 3 real zones now exist (talos-cp-1=az-a, talos-worker-1=az-b,
|
||||||
|
# talos-worker-2=az-c), so anti-affinity keys off zone instead of hostname —
|
||||||
|
# spreads the 3 broker pods one-per-zone/one-per-node (equivalent today,
|
||||||
|
# but zone is the correct long-term key if a node ever gets replaced within
|
||||||
|
# the same zone).
|
||||||
|
antiAffinityTopologyKey: topology.kubernetes.io/zone
|
||||||
|
|
||||||
|
kafka:
|
||||||
|
replicationFactor: 3
|
||||||
|
minInsyncReplicas: 2
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: management-service
|
||||||
|
description: kmsvc message-plane gRPC+REST server (design.md §1, §7a, §9)
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: management-service-config
|
||||||
|
namespace: {{ .Values.namespace }}
|
||||||
|
data:
|
||||||
|
KMSVC_KAFKA_BROKERS: {{ .Values.env.kafkaBrokers | quote }}
|
||||||
|
KMSVC_REDIS_ADDR: {{ .Values.env.redisAddr | quote }}
|
||||||
|
KMSVC_AUTHENTIK_ISSUER_URL: {{ .Values.env.authentikIssuerURL | quote }}
|
||||||
|
KMSVC_AUTHENTIK_AUDIENCE: {{ .Values.env.authentikAudience | quote }}
|
||||||
|
KMSVC_GRPC_LISTEN_ADDR: ":{{ .Values.grpcPort }}"
|
||||||
|
KMSVC_HTTP_LISTEN_ADDR: ":{{ .Values.httpPort }}"
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: management-service
|
||||||
|
namespace: {{ .Values.namespace }}
|
||||||
|
spec:
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: management-service
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: management-service
|
||||||
|
spec:
|
||||||
|
topologySpreadConstraints:
|
||||||
|
- maxSkew: 1
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
whenUnsatisfiable: ScheduleAnyway
|
||||||
|
labelSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: management-service
|
||||||
|
containers:
|
||||||
|
- name: management-service
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
ports:
|
||||||
|
- name: grpc
|
||||||
|
containerPort: {{ .Values.grpcPort }}
|
||||||
|
- name: http
|
||||||
|
containerPort: {{ .Values.httpPort }}
|
||||||
|
env:
|
||||||
|
- name: GOMEMLIMIT
|
||||||
|
value: {{ .Values.goMemLimit | quote }}
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: management-service-config
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: {{ .Values.httpPort }}
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: {{ .Values.httpPort }}
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 20
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{{- if .Values.hpa.enabled }}
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: management-service
|
||||||
|
namespace: {{ .Values.namespace }}
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: management-service
|
||||||
|
minReplicas: {{ .Values.hpa.minReplicas }}
|
||||||
|
maxReplicas: {{ .Values.hpa.maxReplicas }}
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: {{ .Values.hpa.targetCPUUtilizationPercentage }}
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: memory
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: {{ .Values.hpa.targetMemoryUtilizationPercentage }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{{- if and .Values.ingress.enabled .Values.ingress.grpcEnabled }}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: management-service-grpc
|
||||||
|
namespace: {{ .Values.namespace }}
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: {{ .Values.ingress.clusterIssuer }}
|
||||||
|
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
|
||||||
|
spec:
|
||||||
|
ingressClassName: {{ .Values.ingress.className }}
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- {{ .Values.ingress.host }}
|
||||||
|
secretName: {{ .Values.ingress.tlsSecretName }}
|
||||||
|
rules:
|
||||||
|
- host: {{ .Values.ingress.host }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: {{ .Values.ingress.grpcPathPrefix }}
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: management-service
|
||||||
|
port:
|
||||||
|
number: {{ .Values.grpcPort }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{{- if .Values.ingress.enabled }}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: management-service
|
||||||
|
namespace: {{ .Values.namespace }}
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: {{ .Values.ingress.clusterIssuer }}
|
||||||
|
spec:
|
||||||
|
ingressClassName: {{ .Values.ingress.className }}
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- {{ .Values.ingress.host }}
|
||||||
|
secretName: {{ .Values.ingress.tlsSecretName }}
|
||||||
|
rules:
|
||||||
|
- host: {{ .Values.ingress.host }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: management-service
|
||||||
|
port:
|
||||||
|
number: {{ .Values.httpPort }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: management-service
|
||||||
|
namespace: {{ .Values.namespace }}
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: management-service
|
||||||
|
ports:
|
||||||
|
- name: grpc
|
||||||
|
port: {{ .Values.grpcPort }}
|
||||||
|
targetPort: {{ .Values.grpcPort }}
|
||||||
|
- name: http
|
||||||
|
port: {{ .Values.httpPort }}
|
||||||
|
targetPort: {{ .Values.httpPort }}
|
||||||
|
type: ClusterIP
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
namespace: sqs
|
||||||
|
replicaCount: 3
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: ghcr.io/riotpiaole/kmsvc-management-service
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: Always
|
||||||
|
|
||||||
|
grpcPort: 9090
|
||||||
|
httpPort: 8080
|
||||||
|
|
||||||
|
env:
|
||||||
|
kafkaBrokers: "kmsvc-kafka-bootstrap.sqs.svc.cluster.local:9092"
|
||||||
|
redisAddr: "kmsvc-redis-master.sqs.svc.cluster.local:6379"
|
||||||
|
authentikIssuerURL: ""
|
||||||
|
authentikAudience: ""
|
||||||
|
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 200m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: "1"
|
||||||
|
memory: 512Mi
|
||||||
|
|
||||||
|
# Go's GC only reacts to GOGC by default and has no idea about the cgroup
|
||||||
|
# memory limit above -- it'll happily grow heap until the kernel OOMKills it.
|
||||||
|
# Setting GOMEMLIMIT to ~90% of the container limit makes the GC self-throttle
|
||||||
|
# before that happens. Keep this in sync with resources.limits.memory.
|
||||||
|
goMemLimit: "460MiB"
|
||||||
|
|
||||||
|
hpa:
|
||||||
|
enabled: true
|
||||||
|
minReplicas: 3
|
||||||
|
maxReplicas: 9
|
||||||
|
targetCPUUtilizationPercentage: 70
|
||||||
|
targetMemoryUtilizationPercentage: 80
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
className: nginx
|
||||||
|
clusterIssuer: homelab-ca
|
||||||
|
host: kmsvc.riotpiao.com
|
||||||
|
tlsSecretName: kmsvc-tls
|
||||||
|
# kmsvc-cli connects via gRPC directly to --server/KMSVC_SERVER (default
|
||||||
|
# kmsvc.riotpiao.com:443, see kmsvc-cli's README), so raw gRPC needs an
|
||||||
|
# external path too — scoped to the gRPC service's own path prefix on the
|
||||||
|
# same host/port, rather than opening the whole host to gRPC passthrough.
|
||||||
|
grpcEnabled: true
|
||||||
|
grpcPathPrefix: /kafkamgmt.v1.QueueService/
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: queue-crd
|
||||||
|
description: Queue CRD definition + queue-operator Deployment/RBAC (design.md §2a)
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.21.0
|
||||||
|
name: queues.kmsvc.io
|
||||||
|
spec:
|
||||||
|
group: kmsvc.io
|
||||||
|
names:
|
||||||
|
kind: Queue
|
||||||
|
listKind: QueueList
|
||||||
|
plural: queues
|
||||||
|
shortNames:
|
||||||
|
- queue
|
||||||
|
- queues
|
||||||
|
singular: queue
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- jsonPath: .spec.fifoQueue
|
||||||
|
name: FIFO
|
||||||
|
type: boolean
|
||||||
|
- jsonPath: .status.phase
|
||||||
|
name: Phase
|
||||||
|
type: string
|
||||||
|
name: v1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: Queue is the Schema for the queues API — see design.md §2a.
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: |-
|
||||||
|
APIVersion defines the versioned schema of this representation of an object.
|
||||||
|
Servers should convert recognized schemas to the latest internal value, and
|
||||||
|
may reject unrecognized values.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: |-
|
||||||
|
Kind is a string value representing the REST resource this object represents.
|
||||||
|
Servers may infer this from the endpoint the client submits requests to.
|
||||||
|
Cannot be updated.
|
||||||
|
In CamelCase.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
description: QueueSpec defines the desired state of a Queue (design.md
|
||||||
|
§2a).
|
||||||
|
properties:
|
||||||
|
deadLetterTargetQueue:
|
||||||
|
description: |-
|
||||||
|
DeadLetterTargetQueue is the name of another Queue to route exhausted
|
||||||
|
messages to. Must not point at itself or at another DLQ (design.md §5).
|
||||||
|
type: string
|
||||||
|
delaySeconds:
|
||||||
|
description: DelaySeconds is the default delivery delay applied to
|
||||||
|
sent messages.
|
||||||
|
format: int32
|
||||||
|
maximum: 900
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
fifoQueue:
|
||||||
|
default: false
|
||||||
|
description: FIFOQueue enables per-MessageGroupId ordering and deduplication
|
||||||
|
semantics.
|
||||||
|
type: boolean
|
||||||
|
isDLQ:
|
||||||
|
description: |-
|
||||||
|
IsDLQ marks this queue as itself a dead-letter queue, used to enforce
|
||||||
|
the no-DLQ-chaining validation rule in design.md §5.
|
||||||
|
type: boolean
|
||||||
|
maxReceiveCount:
|
||||||
|
default: 5
|
||||||
|
description: |-
|
||||||
|
MaxReceiveCount is how many times a message may be redelivered before
|
||||||
|
being routed to DeadLetterTargetQueue.
|
||||||
|
format: int32
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
maxShards:
|
||||||
|
default: 8
|
||||||
|
description: MaxShards is the ceiling on shard count the operator
|
||||||
|
may split up to (design.md §2c).
|
||||||
|
format: int32
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
messageRetentionPeriodSeconds:
|
||||||
|
default: 345600
|
||||||
|
description: MessageRetentionPeriodSeconds maps to the underlying
|
||||||
|
Kafka topic's retention.ms.
|
||||||
|
format: int32
|
||||||
|
maximum: 1209600
|
||||||
|
minimum: 60
|
||||||
|
type: integer
|
||||||
|
minShards:
|
||||||
|
default: 1
|
||||||
|
description: MinShards is the floor on shard count; the operator never
|
||||||
|
merges below this.
|
||||||
|
format: int32
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
partitionsPerShard:
|
||||||
|
default: 6
|
||||||
|
description: PartitionsPerShard is the Kafka partition count on each
|
||||||
|
shard's topic.
|
||||||
|
format: int32
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
shardSplitCooldownSeconds:
|
||||||
|
default: 300
|
||||||
|
description: |-
|
||||||
|
ShardSplitCooldownSeconds is the minimum age a shard must reach before it
|
||||||
|
is eligible to be split again, preventing rapid re-splitting of a child
|
||||||
|
that hasn't yet absorbed its share of traffic.
|
||||||
|
format: int32
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
shardSplitThresholdBytesPerSec:
|
||||||
|
default: 5242880
|
||||||
|
description: |-
|
||||||
|
ShardSplitThresholdBytesPerSec is the sustained per-shard throughput that
|
||||||
|
triggers a split into two child shards (design.md §2c).
|
||||||
|
format: int64
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
visibilityTimeoutSeconds:
|
||||||
|
default: 30
|
||||||
|
description: |-
|
||||||
|
VisibilityTimeoutSeconds is how long a received-but-unacked message stays
|
||||||
|
invisible to other consumers before being redelivered.
|
||||||
|
format: int32
|
||||||
|
maximum: 43200
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: QueueStatus defines the observed state of a Queue.
|
||||||
|
properties:
|
||||||
|
conditions:
|
||||||
|
description: Conditions hold detailed status information.
|
||||||
|
items:
|
||||||
|
description: Condition contains details for one aspect of the current
|
||||||
|
state of this API Resource.
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
description: |-
|
||||||
|
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||||
|
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
description: |-
|
||||||
|
message is a human readable message indicating details about the transition.
|
||||||
|
This may be an empty string.
|
||||||
|
maxLength: 32768
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: |-
|
||||||
|
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||||
|
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||||
|
with respect to the current state of the instance.
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
reason:
|
||||||
|
description: |-
|
||||||
|
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||||
|
Producers of specific condition types may define expected values and meanings for this field,
|
||||||
|
and whether the values are considered a guaranteed API.
|
||||||
|
The value should be a CamelCase string.
|
||||||
|
This field may not be empty.
|
||||||
|
maxLength: 1024
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: status of the condition, one of True, False, Unknown.
|
||||||
|
enum:
|
||||||
|
- "True"
|
||||||
|
- "False"
|
||||||
|
- Unknown
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||||
|
maxLength: 316
|
||||||
|
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- lastTransitionTime
|
||||||
|
- message
|
||||||
|
- reason
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
phase:
|
||||||
|
description: Phase is the current reconciliation phase.
|
||||||
|
enum:
|
||||||
|
- Pending
|
||||||
|
- Ready
|
||||||
|
- Failed
|
||||||
|
type: string
|
||||||
|
shards:
|
||||||
|
description: |-
|
||||||
|
Shards lists every shard backing this queue, active or draining
|
||||||
|
(design.md §2a/§2c).
|
||||||
|
items:
|
||||||
|
description: ShardStatus describes one shard backing a Queue (design.md
|
||||||
|
§2a/§2c).
|
||||||
|
properties:
|
||||||
|
availabilityZones:
|
||||||
|
description: |-
|
||||||
|
AvailabilityZones lists the topology.kubernetes.io/zone values of every
|
||||||
|
node currently hosting a Kafka replica of this shard's topic, resolved
|
||||||
|
from the broker pods' node placement each reconcile. Empty until the
|
||||||
|
first successful resolution (e.g. node lookup failed transiently).
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
createdAt:
|
||||||
|
description: |-
|
||||||
|
CreatedAt timestamps when this shard was created, used to enforce
|
||||||
|
ShardSplitCooldownSeconds.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
hashRangeEnd:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
hashRangeStart:
|
||||||
|
description: |-
|
||||||
|
HashRangeStart/HashRangeEnd define the [start, end) murmur2 hash range
|
||||||
|
this shard owns over the 32-bit key space. Stored as int64 (not uint32)
|
||||||
|
because controller-gen maps Go uint32 to OpenAPI format:int32, whose max
|
||||||
|
(2147483647) is smaller than FullHashRangeEnd (0xFFFFFFFF) and the
|
||||||
|
apiserver rejects the status update.
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
id:
|
||||||
|
description: ID is the shard's identifier, used in its topic
|
||||||
|
name (kmsvc.{queue}.shard-{id}).
|
||||||
|
type: string
|
||||||
|
parentId:
|
||||||
|
description: |-
|
||||||
|
ParentID is the shard ID this shard was split from, empty for the
|
||||||
|
original shard-0.
|
||||||
|
type: string
|
||||||
|
phase:
|
||||||
|
description: Phase is this shard's lifecycle state.
|
||||||
|
enum:
|
||||||
|
- Active
|
||||||
|
- Closing
|
||||||
|
- Closed
|
||||||
|
type: string
|
||||||
|
topic:
|
||||||
|
description: Topic is the underlying Kafka topic name for this
|
||||||
|
shard.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- hashRangeEnd
|
||||||
|
- hashRangeStart
|
||||||
|
- id
|
||||||
|
- phase
|
||||||
|
- topic
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: queue-operator
|
||||||
|
namespace: {{ .Values.namespace }}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: queue-operator
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: queue-operator
|
||||||
|
spec:
|
||||||
|
serviceAccountName: queue-operator
|
||||||
|
containers:
|
||||||
|
- name: queue-operator
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
command: ["/queue-operator"]
|
||||||
|
env:
|
||||||
|
- name: KMSVC_KAFKA_BROKERS
|
||||||
|
value: {{ .Values.kafkaBrokers | quote }}
|
||||||
|
- name: KMSVC_REDIS_ADDR
|
||||||
|
value: {{ .Values.redisAddr | quote }}
|
||||||
|
- name: GOMEMLIMIT
|
||||||
|
value: {{ .Values.goMemLimit | quote }}
|
||||||
|
- name: KMSVC_NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
- name: KMSVC_KAFKA_CLUSTER_NAME
|
||||||
|
value: {{ .Values.kafkaClusterName | quote }}
|
||||||
|
- name: KMSVC_KAFKA_POOL_NAME
|
||||||
|
value: {{ .Values.kafkaPoolName | quote }}
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: queue-operator
|
||||||
|
namespace: {{ .Values.namespace }}
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: queue-operator
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["kmsvc.io"]
|
||||||
|
resources: ["queues"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
- apiGroups: ["kmsvc.io"]
|
||||||
|
resources: ["queues/status"]
|
||||||
|
verbs: ["get", "update", "patch"]
|
||||||
|
- apiGroups: ["kmsvc.io"]
|
||||||
|
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"]
|
||||||
|
- apiGroups: ["coordination.k8s.io"]
|
||||||
|
resources: ["leases"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["events"]
|
||||||
|
verbs: ["create", "patch"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["pods", "nodes"]
|
||||||
|
verbs: ["get"]
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources: ["deployments"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: queue-operator
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: queue-operator
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: queue-operator
|
||||||
|
namespace: {{ .Values.namespace }}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: temporalworkers.kmsvc.io
|
||||||
|
spec:
|
||||||
|
group: kmsvc.io
|
||||||
|
names:
|
||||||
|
kind: TemporalWorker
|
||||||
|
plural: temporalworkers
|
||||||
|
singular: temporalworker
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- name: v1
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- spec
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
type: object
|
||||||
|
description: Temporal worker specification
|
||||||
|
properties:
|
||||||
|
namespace:
|
||||||
|
type: string
|
||||||
|
description: Temporal namespace
|
||||||
|
taskQueue:
|
||||||
|
type: string
|
||||||
|
description: Task queue name
|
||||||
|
workflowTypes:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: List of workflow types to execute
|
||||||
|
activityTypes:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: List of activity types to execute
|
||||||
|
concurrency:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
description: Worker concurrency level
|
||||||
|
status:
|
||||||
|
type: object
|
||||||
|
description: Temporal worker status
|
||||||
|
properties:
|
||||||
|
ready:
|
||||||
|
type: boolean
|
||||||
|
lastHeartbeat:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
namespace: sqs
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: ghcr.io/riotpiaole/kmsvc-management-service
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: Always
|
||||||
|
|
||||||
|
kafkaBrokers: "kmsvc-kafka-bootstrap.sqs.svc.cluster.local:9092"
|
||||||
|
redisAddr: "kmsvc-redis-master.sqs.svc.cluster.local:6379"
|
||||||
|
|
||||||
|
# Must match kafka-cluster chart's clusterName/derived pool name -- used to
|
||||||
|
# resolve "<kafkaClusterName>-<kafkaPoolName>-<brokerID>" broker pod names
|
||||||
|
# for AZ-aware Queue status (design.md §2a).
|
||||||
|
kafkaClusterName: kmsvc
|
||||||
|
kafkaPoolName: kmsvc-pool
|
||||||
|
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 256Mi
|
||||||
|
|
||||||
|
# See management-service/values.yaml's goMemLimit comment -- same reasoning.
|
||||||
|
goMemLimit: "230MiB"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: portainer
|
||||||
|
resources: []
|
||||||
|
# Portainer deployed via Helm chart or existing manifests
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# k8s/portainer/portainer-values.yaml
|
||||||
|
# Portainer — web UI for browsing cluster workloads, exec-ing into pods,
|
||||||
|
# and viewing logs without kubectl. Operator-only access (ClusterIP + port-forward).
|
||||||
|
#
|
||||||
|
# Node failure behaviour:
|
||||||
|
# Portainer is a Deployment (not StatefulSet), so K8s auto-evicts and
|
||||||
|
# reschedules it ~5 min after a node becomes unreachable. Longhorn
|
||||||
|
# reattaches the PVC on the new node in ~1-2 min. Worst case: ~7-10 min.
|
||||||
|
#
|
||||||
|
# To cut that down: in Longhorn UI → Settings set
|
||||||
|
# nodeDownPodDeletionPolicy = delete-deployment-pod
|
||||||
|
# Longhorn will force-delete the stuck pod immediately when the node is
|
||||||
|
# fenced rather than waiting for Kubernetes' eviction timeout.
|
||||||
|
|
||||||
|
# ── Service ───────────────────────────────────────────────────────────────────
|
||||||
|
# ClusterIP — no external exposure. Access via:
|
||||||
|
# kubectl -n dashboard port-forward svc/portainer 9000:9000
|
||||||
|
# Portainer holds cluster-admin credentials; never expose as LoadBalancer.
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
|
||||||
|
# ── TLS ───────────────────────────────────────────────────────────────────────
|
||||||
|
# Portainer by default redirects HTTP → HTTPS using a self-signed cert.
|
||||||
|
# force: false disables the redirect so plain HTTP over port-forward works
|
||||||
|
# without browser cert warnings. TLS is terminated at the ingress layer
|
||||||
|
# if/when an ingress rule is added.
|
||||||
|
tls:
|
||||||
|
force: false
|
||||||
|
|
||||||
|
# ── Persistence ───────────────────────────────────────────────────────────────
|
||||||
|
# Stores Portainer's own config: environment registrations, user accounts,
|
||||||
|
# stack definitions, and access control settings. Longhorn provides the
|
||||||
|
# RWO block volume. 10Gi is generous for config data but cheap on Longhorn.
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
storageClass: "longhorn"
|
||||||
|
size: 10Gi
|
||||||
|
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 512Mi
|
||||||
|
|
||||||
|
# ── Scheduling ────────────────────────────────────────────────────────────────
|
||||||
|
# Allow scheduling on talos-cp-1 (carries NoSchedule taint) so Portainer
|
||||||
|
# keeps running even when the worker node is down.
|
||||||
|
tolerations:
|
||||||
|
- key: node-role.kubernetes.io/control-plane
|
||||||
|
operator: Exists
|
||||||
|
effect: NoSchedule
|
||||||
|
|
||||||
|
# Pin to az-b (talos-cp-2) — sole Longhorn storage node (dedicated disks).
|
||||||
|
# Its RWO PVC can only attach there; without this the pod may land on
|
||||||
|
# cp-1/cp-3 and fail to mount.
|
||||||
|
nodeSelector:
|
||||||
|
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"
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# k8s/temporal/temporal-values.yaml
|
||||||
|
# Temporal — workflow engine
|
||||||
|
# Uses external CNPG PostgreSQL for persistence (temporal-db)
|
||||||
|
# Visibility via same PostgreSQL instance, separate database.
|
||||||
|
#
|
||||||
|
# IMPORTANT — chart schema note (root-caused after Postgres never actually
|
||||||
|
# taking effect despite looking configured):
|
||||||
|
# We're pinned to temporalio/helm-charts @ 0.74.0 (see targetRevision in
|
||||||
|
# k8s/argocd/apps/60-applications.yaml), which uses the OLD flat persistence
|
||||||
|
# schema:
|
||||||
|
# server.config.persistence.<default|visibility>.driver: "sql"|"cassandra"
|
||||||
|
# server.config.persistence.<default|visibility>.sql: {...}
|
||||||
|
# NOT the newer `datastores:`-wrapped schema
|
||||||
|
# (server.config.persistence.datastores.<store>.sql) shown in the current
|
||||||
|
# chart's values/values.postgresql.yaml example - that key was introduced in
|
||||||
|
# a later major version and doesn't exist in 0.74.0. Helm doesn't validate
|
||||||
|
# unknown keys, so a `datastores:` block here is silently a no-op: Temporal
|
||||||
|
# would keep defaulting to Cassandra (with empty hosts: []) regardless of
|
||||||
|
# anything nested inside it. Verified via `helm template` against the actual
|
||||||
|
# 0.74.0 chart before writing this file - see chat history for the
|
||||||
|
# side-by-side proof (rendered manifest showed CASSANDRA_HOST env vars and
|
||||||
|
# temporal-cassandra-tool commands using the old datastores:-based values).
|
||||||
|
#
|
||||||
|
# Likewise `schema.setup.enabled` / `schema.update.enabled` /
|
||||||
|
# `schema.createDatabase.enabled` are the real toggles for the schema-setup
|
||||||
|
# Job (all default true) - there is no `jobs.autoSetup` key in this chart.
|
||||||
|
|
||||||
|
# ── Disable every bundled/optional sub-chart ─────────────────────────────────
|
||||||
|
# postgresql/mysql: never enable - we never want the chart to deploy its own
|
||||||
|
# DB, only to know how to talk to our external CNPG instance (which happens
|
||||||
|
# via server.config.persistence.*.sql below, independent of these flags).
|
||||||
|
postgresql:
|
||||||
|
enabled: false
|
||||||
|
mysql:
|
||||||
|
enabled: false
|
||||||
|
cassandra:
|
||||||
|
enabled: false
|
||||||
|
elasticsearch:
|
||||||
|
enabled: false
|
||||||
|
prometheus:
|
||||||
|
enabled: false
|
||||||
|
grafana:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# ── Schema setup/update Jobs ──────────────────────────────────────────────────
|
||||||
|
# The `temporal` DB is created by the dedicated temporal-db cluster's initdb and
|
||||||
|
# `temporal_visibility` by a CNPG Database CR — both in
|
||||||
|
# k8s/infra/databases/temporal-db.yaml — so createDatabase stays disabled.
|
||||||
|
# setup/update run temporal-sql-tool as the `app` owner against those existing
|
||||||
|
# DBs to install and migrate the
|
||||||
|
# Temporal server schema — without them both DBs have zero tables and the
|
||||||
|
# server dies on "no usable database connection found" (no schema_version row).
|
||||||
|
schema:
|
||||||
|
createDatabase:
|
||||||
|
enabled: false
|
||||||
|
setup:
|
||||||
|
enabled: true
|
||||||
|
update:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# ── Temporal server config (PostgreSQL persistence) ──────────────────────────
|
||||||
|
server:
|
||||||
|
replicaCount: 1
|
||||||
|
# temporalio/server:1.30.0+ dropped the `dockerize` binary and switched to
|
||||||
|
# built-in sprig config templating. The chart still defaults to the legacy
|
||||||
|
# configMapsToMount: "dockerize" + setConfigFilePath: false, which produces a
|
||||||
|
# config the 1.30 server never loads — it then falls back to its embedded
|
||||||
|
# env-only template (Cassandra default) and dies with
|
||||||
|
# "Persistence.DataStores[default](value).Cassandra.Hosts: zero value".
|
||||||
|
# Switch to the sprig ConfigMap and point the server at it (chart's own
|
||||||
|
# recommendation for 1.30.0+ images; sprig mode requires setConfigFilePath).
|
||||||
|
configMapsToMount: "sprig"
|
||||||
|
setConfigFilePath: true
|
||||||
|
jobService:
|
||||||
|
enabled: false
|
||||||
|
affinity:
|
||||||
|
podAntiAffinity:
|
||||||
|
preferredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
- weight: 100
|
||||||
|
podAffinityTerm:
|
||||||
|
labelSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/instance: temporal
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
config:
|
||||||
|
logLevel: "info"
|
||||||
|
persistence:
|
||||||
|
defaultStore: default
|
||||||
|
visibilityStore: visibility
|
||||||
|
numHistoryShards: 512
|
||||||
|
default:
|
||||||
|
driver: "sql"
|
||||||
|
sql:
|
||||||
|
driver: "postgres12"
|
||||||
|
host: "temporal-db-rw.temporal.svc.cluster.local"
|
||||||
|
port: 5432
|
||||||
|
database: "temporal"
|
||||||
|
user: "app"
|
||||||
|
# existingSecret + secretKey: point directly at the CNPG-generated
|
||||||
|
# Secret (kubernetes.io/basic-auth, keys: username/password/...)
|
||||||
|
# rather than duplicating the password in git as plaintext. When
|
||||||
|
# existingSecret is set the chart's own server-secret.yaml Secret
|
||||||
|
# template is skipped entirely (see templates/server-secret.yaml:
|
||||||
|
# `not $driverConfig.existingSecret` guards its creation).
|
||||||
|
# Use unified temporal-db-app secret (generated in temporal namespace)
|
||||||
|
existingSecret: "temporal-db-app"
|
||||||
|
secretKey: "password"
|
||||||
|
maxConns: 20
|
||||||
|
maxIdleConns: 10
|
||||||
|
maxConnLifetime: "1h"
|
||||||
|
# NOTE: no `connectAttributes: { tx_isolation: ... }` here — tx_isolation
|
||||||
|
# is a MySQL-only connection parameter. The Postgres `pq` driver rejects
|
||||||
|
# it ("unrecognized configuration parameter"), which killed every DB
|
||||||
|
# connection (schema-setup job AND server) with the misleading
|
||||||
|
# "no usable database connection found". Postgres defaults to READ
|
||||||
|
# COMMITTED isolation anyway, so nothing is lost by omitting it.
|
||||||
|
visibility:
|
||||||
|
driver: "sql"
|
||||||
|
sql:
|
||||||
|
driver: "postgres12"
|
||||||
|
host: "temporal-db-rw.temporal.svc.cluster.local"
|
||||||
|
port: 5432
|
||||||
|
database: "temporal_visibility"
|
||||||
|
user: "app"
|
||||||
|
# Use unified temporal-db-app secret (generated in temporal namespace)
|
||||||
|
existingSecret: "temporal-db-app"
|
||||||
|
secretKey: "password"
|
||||||
|
maxConns: 20
|
||||||
|
maxIdleConns: 10
|
||||||
|
maxConnLifetime: "1h"
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
|
||||||
|
# ── Temporal Web UI ────────────────────────────────────────────────────────
|
||||||
|
web:
|
||||||
|
replicaCount: 1
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
|
||||||
|
# ── Ingress ────────────────────────────────────────────────────────
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: sops-secrets
|
||||||
|
namespace: argocd
|
||||||
|
finalizers:
|
||||||
|
- resources-finalizer.argocd.argoproj.io
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "0"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
revisionHistoryLimit: 3
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
source:
|
||||||
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
# ksops decrypts every *.enc.yaml here at kustomize-build time (repo-server
|
||||||
|
# runs `kustomize build --enable-alpha-plugins --enable-exec`). Replaces the
|
||||||
|
# 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:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# Wave 0/1 — cluster substrate: cert-manager, ingress-nginx, reloader, and the
|
||||||
|
# Let's Encrypt issuers + wildcard cert. Previously installed by Terraform; now
|
||||||
|
# owned by app-of-apps (Pure GitOps). Controllers at wave 0; the ClusterIssuers
|
||||||
|
# and wildcard Certificate at wave 1 so cert-manager CRDs exist first.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: cert-manager
|
||||||
|
namespace: argocd
|
||||||
|
finalizers:
|
||||||
|
- resources-finalizer.argocd.argoproj.io
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "0"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
revisionHistoryLimit: 3
|
||||||
|
sources:
|
||||||
|
- repoURL: https://charts.jetstack.io
|
||||||
|
chart: cert-manager
|
||||||
|
targetRevision: "v1.21.0"
|
||||||
|
helm:
|
||||||
|
valueFiles:
|
||||||
|
- $values/k8s/bootstrap/cert-manager/cert-manager-values.yaml
|
||||||
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
ref: values
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: cert-manager
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
- ServerSideApply=true
|
||||||
|
# ingress-nginx removed: duplicate of ingress-nginx-bootstrap
|
||||||
|
# The bootstrap version (k8s/bootstrap-local/06-ingress-nginx.yaml) is kept
|
||||||
|
# to break the circular dependency (ArgoCD needs Forgejo domain access)
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: reloader
|
||||||
|
namespace: argocd
|
||||||
|
finalizers:
|
||||||
|
- resources-finalizer.argocd.argoproj.io
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "0"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
revisionHistoryLimit: 3
|
||||||
|
source:
|
||||||
|
repoURL: https://stakater.github.io/stakater-charts
|
||||||
|
chart: reloader
|
||||||
|
targetRevision: "2.2.14"
|
||||||
|
helm:
|
||||||
|
values: |
|
||||||
|
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:
|
||||||
|
tolerations:
|
||||||
|
- key: node-role.kubernetes.io/control-plane
|
||||||
|
operator: Exists
|
||||||
|
effect: NoSchedule
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: reloader
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
---
|
||||||
|
# Wave 1 — LE ClusterIssuers + wildcard cert (needs cert-manager CRDs from wave 0).
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: cert-manager-issuers
|
||||||
|
namespace: argocd
|
||||||
|
finalizers:
|
||||||
|
- resources-finalizer.argocd.argoproj.io
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "1"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
revisionHistoryLimit: 3
|
||||||
|
source:
|
||||||
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
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
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: cert-manager
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
---
|
||||||
|
# Consolidated: wildcard-cert + homelab-ingress → ingress-config
|
||||||
|
# Manages both the wildcard TLS certificate and all Ingress rules.
|
||||||
|
# Certificate must exist before Ingresses (wave 1), but both are in same directory.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: ingress-config
|
||||||
|
namespace: argocd
|
||||||
|
finalizers:
|
||||||
|
- resources-finalizer.argocd.argoproj.io
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "1"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
revisionHistoryLimit: 3
|
||||||
|
source:
|
||||||
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/bootstrap/ingress
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Wave 0 — networking substrate is Talos-owned (terraform inlineManifests), not
|
||||||
|
# ArgoCD:
|
||||||
|
# - CoreDNS Corefile + hostname rewrites -> terraform/files/coredns/Corefile
|
||||||
|
# - Cilium LB-IPAM pool + L2 announcement -> terraform/files/cilium/*.yaml
|
||||||
|
# Both were previously ArgoCD apps here whose empty `resources: []`
|
||||||
|
# kustomizations never actually applied them (live objects came from manual
|
||||||
|
# kubectl). Managing them from ArgoCD too would let two reconcilers fight. This
|
||||||
|
# file intentionally defines no Applications now.
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
# Wave 1 — MinIO (operator + tenant), Longhorn policy, Prometheus stack.
|
||||||
|
# Helm charts pull from public repos; values come from the git repo via a
|
||||||
|
# second "ref: values" source (ArgoCD multi-source pattern).
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: minio-operator
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "1"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
sources:
|
||||||
|
- repoURL: https://operator.min.io/
|
||||||
|
chart: operator
|
||||||
|
targetRevision: "5.0.18"
|
||||||
|
helm:
|
||||||
|
valueFiles:
|
||||||
|
- $values/k8s/infra/minio/minio-operator-values.yaml
|
||||||
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
ref: values
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: storage
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
---
|
||||||
|
# Tenant + buckets + replication are raw CRs (MinIO Tenant CRD from operator).
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: minio-tenant
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "1"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/infra/minio
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: storage
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
---
|
||||||
|
# Longhorn itself is substrate (bootstrap-installed); this app manages only its
|
||||||
|
# ServiceMonitor / policy manifests.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: longhorn-config
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "1"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/infra/longhorn
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
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:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: prometheus
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "1"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
sources:
|
||||||
|
- repoURL: https://prometheus-community.github.io/helm-charts
|
||||||
|
chart: kube-prometheus-stack
|
||||||
|
targetRevision: "*"
|
||||||
|
helm:
|
||||||
|
skipCrds: true
|
||||||
|
valueFiles:
|
||||||
|
- $values/k8s/infra/monitoring/prometheus-values.yaml
|
||||||
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
ref: values
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: monitoring
|
||||||
|
syncPolicy:
|
||||||
|
managedNamespaceMetadata:
|
||||||
|
# node-exporter needs hostNetwork/hostPID/hostPath/hostPort; blocked by
|
||||||
|
# default baseline PSS (DaemonSet created 0 pods, Prometheus STS stuck).
|
||||||
|
labels:
|
||||||
|
pod-security.kubernetes.io/enforce: privileged
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
# ServerSideApply removed — it conflicts with managedNamespaceMetadata's
|
||||||
|
# forced namespace apply ("--force cannot be used with --server-side").
|
||||||
|
# helm.skipCrds: true above stops ArgoCD from ever managing the CRDs
|
||||||
|
# through this Application (previously it kept re-patching them via
|
||||||
|
# client-side apply and hitting etcd's 262144-byte annotation limit on
|
||||||
|
# kubectl.kubernetes.io/last-applied-configuration, permanently failing
|
||||||
|
# sync). CRDs are applied once via the separate prometheus-crds
|
||||||
|
# Application below, which uses ServerSideApply=true (no namespace-
|
||||||
|
# metadata conflict since CRDs are cluster-scoped).
|
||||||
|
---
|
||||||
|
# CRDs only, extracted to plain YAML (`helm show crds kube-prometheus-stack`)
|
||||||
|
# and committed to git under k8s/infra/monitoring/crds/, applied via Server-
|
||||||
|
# Side Apply to avoid the etcd 262144-byte last-applied-configuration
|
||||||
|
# annotation limit that client-side apply hits on these very large CRDs
|
||||||
|
# (prometheuses, alertmanagers, scrapeconfigs, etc). A plain git path source
|
||||||
|
# (not a remote Helm source) is used deliberately so ArgoCD applies exactly
|
||||||
|
# these 8 CRD manifests and nothing else — no ambiguity about what "CRDs only"
|
||||||
|
# means from a Helm chart. Split out from the main `prometheus` Application
|
||||||
|
# (helm.skipCrds: true there) because ServerSideApply conflicts with that
|
||||||
|
# app's managedNamespaceMetadata.
|
||||||
|
# NOTE: bump k8s/infra/monitoring/crds/kube-prometheus-stack-crds.yaml
|
||||||
|
# whenever the kube-prometheus-stack chart version changes materially
|
||||||
|
# (`helm show crds prometheus-community/kube-prometheus-stack > ...`).
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: prometheus-crds
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "0"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/infra/monitoring/crds
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: monitoring
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
- ServerSideApply=true
|
||||||
|
---
|
||||||
|
# Cluster monitoring config: custom PrometheusRules (per-app namespaces),
|
||||||
|
# ServiceMonitors (monitoring ns), and Grafana dashboard ConfigMaps (logging ns,
|
||||||
|
# grafana sidecar-discovered). Single source = k8s/infra/monitoring (one
|
||||||
|
# kustomization, no namespace transformer so per-app rule namespaces are kept).
|
||||||
|
# Wave 2: after prometheus-operator CRDs (wave 0) + stack (wave 1) and grafana
|
||||||
|
# (wave 2, logging). ServerSideApply avoids the etcd last-applied annotation
|
||||||
|
# limit on the large dashboard ConfigMap JSON.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: monitoring-config
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "2"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/infra/monitoring
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: monitoring
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
- ServerSideApply=true
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: blackbox-exporter
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "1"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
sources:
|
||||||
|
- repoURL: https://prometheus-community.github.io/helm-charts
|
||||||
|
chart: prometheus-blackbox-exporter
|
||||||
|
targetRevision: "~11"
|
||||||
|
helm:
|
||||||
|
valueFiles:
|
||||||
|
- $values/k8s/infra/monitoring/blackbox-exporter-values.yaml
|
||||||
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
ref: values
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: monitoring
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Wave 2 — Loki / Grafana / Promtail (Grafana Helm charts).
|
||||||
|
# NOTE: loki-values / grafana-values reference secrets (S3 creds, admin password)
|
||||||
|
# that helmfile used to inject via --set. Under ArgoCD these come from the
|
||||||
|
# *.enc.yaml SOPS files in the same dir via the SOPS plugin — verify the plugin
|
||||||
|
# is configured before first sync, or these will render with empty secrets.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: loki
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "2"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
sources:
|
||||||
|
- repoURL: https://grafana.github.io/helm-charts
|
||||||
|
chart: loki
|
||||||
|
targetRevision: "*"
|
||||||
|
helm:
|
||||||
|
valueFiles:
|
||||||
|
- $values/k8s/infra/logging/loki-values.yaml
|
||||||
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
ref: values
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: logging
|
||||||
|
syncPolicy:
|
||||||
|
managedNamespaceMetadata:
|
||||||
|
# promtail needs privileged (hostPath log/journal, DAC_READ_SEARCH,
|
||||||
|
# privileged:true) to tail node logs — default baseline PSS blocks it.
|
||||||
|
labels:
|
||||||
|
pod-security.kubernetes.io/enforce: privileged
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: grafana
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "2"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
sources:
|
||||||
|
- repoURL: https://grafana.github.io/helm-charts
|
||||||
|
chart: grafana
|
||||||
|
targetRevision: "*"
|
||||||
|
helm:
|
||||||
|
valueFiles:
|
||||||
|
- $values/k8s/infra/logging/grafana-values.yaml
|
||||||
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
ref: values
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: logging
|
||||||
|
syncPolicy:
|
||||||
|
managedNamespaceMetadata:
|
||||||
|
# promtail needs privileged (hostPath log/journal, DAC_READ_SEARCH,
|
||||||
|
# privileged:true) to tail node logs — default baseline PSS blocks it.
|
||||||
|
labels:
|
||||||
|
pod-security.kubernetes.io/enforce: privileged
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: promtail
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "2"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
sources:
|
||||||
|
- repoURL: https://grafana.github.io/helm-charts
|
||||||
|
chart: promtail
|
||||||
|
targetRevision: "*"
|
||||||
|
helm:
|
||||||
|
valueFiles:
|
||||||
|
- $values/k8s/infra/logging/promtail-values.yaml
|
||||||
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
ref: values
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: logging
|
||||||
|
syncPolicy:
|
||||||
|
managedNamespaceMetadata:
|
||||||
|
# promtail needs privileged (hostPath log/journal, DAC_READ_SEARCH,
|
||||||
|
# privileged:true) to tail node logs — default baseline PSS blocks it.
|
||||||
|
labels:
|
||||||
|
pod-security.kubernetes.io/enforce: privileged
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# Wave 3 — Vault + Authentik (identity), plus IAM raw jobs and the Forgejo
|
||||||
|
# runner. Authentik/Vault values reference SOPS-managed secrets (see *.enc.yaml
|
||||||
|
# in k8s/infra/iam) resolved by the ArgoCD SOPS plugin at sync time.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: vault
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "3"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
sources:
|
||||||
|
- repoURL: https://helm.releases.hashicorp.com
|
||||||
|
chart: vault
|
||||||
|
targetRevision: "*"
|
||||||
|
helm:
|
||||||
|
valueFiles:
|
||||||
|
- $values/k8s/infra/iam/vault-values.yaml
|
||||||
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
ref: values
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: iam
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: authentik
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "3"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
sources:
|
||||||
|
- repoURL: https://charts.goauthentik.io
|
||||||
|
chart: authentik
|
||||||
|
targetRevision: "*"
|
||||||
|
helm:
|
||||||
|
valueFiles:
|
||||||
|
- $values/k8s/infra/iam/authentik-values.yaml
|
||||||
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
ref: values
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: iam
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
---
|
||||||
|
# Raw IAM manifests: key-rotation cronjob + authentik migration job.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: iam-jobs
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "3"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/infra/iam
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: iam
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
---
|
||||||
|
# Forgejo runner (local chart). Forgejo itself is Phase 0 (bootstrap).
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: forgejo-runner
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "3"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/infra/forgejo-runner
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: cicd
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Wave 2 — dedicated per-app CNPG Postgres clusters (authentik-db → ns iam,
|
||||||
|
# temporal-db + visibility → ns temporal). ONE App, ONE folder (k8s/infra/databases).
|
||||||
|
# CNPG operator is Phase-0 bootstrap; these Cluster CRs are GitOps — no circular
|
||||||
|
# dep (they run after ArgoCD is up, before their apps at w3/w8). CNPG generates
|
||||||
|
# each cluster's `<name>-app` secret + `<name>-rw` service in-namespace; the apps
|
||||||
|
# read them locally. Forgejo's DB stays separate (bootstrap/circular).
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: databases
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "2"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/infra/databases
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: default
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- ServerSideApply=true
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# Wave 5 — Kafka (Strimzi operator + cluster CR), Redis, and the SQS-like
|
||||||
|
# queue services. Strimzi/Redis are public Helm charts; kafka-cluster/queue-crd/
|
||||||
|
# management-service are local charts (rendered from their own Chart.yaml).
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: strimzi-operator
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "5"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: https://strimzi.io/charts/
|
||||||
|
chart: strimzi-kafka-operator
|
||||||
|
targetRevision: 0.46.0
|
||||||
|
helm:
|
||||||
|
releaseName: strimzi-operator
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: sqs
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: kmsvc-redis
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "5"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: https://charts.bitnami.com/bitnami
|
||||||
|
chart: redis
|
||||||
|
targetRevision: 20.6.0
|
||||||
|
helm:
|
||||||
|
# docker.io/bitnami stopped publishing version-pinned tags; bitnamilegacy
|
||||||
|
# mirrors them. allowInsecureImages silences the chart image-allowlist check.
|
||||||
|
values: |
|
||||||
|
global:
|
||||||
|
security:
|
||||||
|
allowInsecureImages: true
|
||||||
|
image:
|
||||||
|
repository: bitnamilegacy/redis
|
||||||
|
auth:
|
||||||
|
enabled: false
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: sqs
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: kafka-cluster
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "6"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/apps/messaging/kafka-cluster
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: sqs
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: queue-crd
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "6"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/apps/messaging/queue-crd
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: sqs
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: management-service
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "7"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/apps/messaging/management-service
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: sqs
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
# Wave 8 — end-user workloads: Temporal, Portainer, and the cloudflared tunnel.
|
||||||
|
# Experimental dirs (llm, forge, dev-tools, shadowsocks) are intentionally
|
||||||
|
# NOT included yet — add them here once they're production-ready.
|
||||||
|
# temporal using unified CNPG pattern (app user, temporal-db-app secret)
|
||||||
|
# Secret copied by bootstrap.sh (like cicd/iam namespaces)
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: temporal
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "8"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
sources:
|
||||||
|
- repoURL: https://go.temporal.io/helm-charts
|
||||||
|
chart: temporal
|
||||||
|
targetRevision: "0.74.0"
|
||||||
|
helm:
|
||||||
|
valueFiles:
|
||||||
|
- $values/k8s/apps/temporal/temporal-values.yaml
|
||||||
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
ref: values
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: temporal
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: portainer
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "8"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
sources:
|
||||||
|
- repoURL: https://portainer.github.io/k8s/
|
||||||
|
chart: portainer
|
||||||
|
targetRevision: "*"
|
||||||
|
helm:
|
||||||
|
valueFiles:
|
||||||
|
- $values/k8s/apps/portainer/portainer-values.yaml
|
||||||
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
ref: values
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: dashboard
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: cloudflared
|
||||||
|
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/cloudflared
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: cloudflared
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: agent-pod
|
||||||
|
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/agent-pod
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: agent-pod
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- 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
|
||||||
|
# Helm chart + values + PostSync hook patch (fix-probes-job.yaml)
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: homarr
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "8"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
sources:
|
||||||
|
- repoURL: https://homarr-labs.github.io/charts
|
||||||
|
chart: homarr
|
||||||
|
targetRevision: "*"
|
||||||
|
helm:
|
||||||
|
valueFiles:
|
||||||
|
- $values/k8s/apps/homarr/homarr-values.yaml
|
||||||
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
ref: values
|
||||||
|
- repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/apps/homarr # PostSync hook: fix-probes-job.yaml
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: dashboard
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# k8s/argocd/projects/homelab-project.yaml
|
||||||
|
# AppProject referenced by every Application manifest under k8s/argocd/apps/
|
||||||
|
# (project: homelab) — was never committed, so 00-homelab-root.yaml and all
|
||||||
|
# layer/wave/phase apps fail admission with "application references project
|
||||||
|
# 'homelab' which does not exist" until this exists.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: AppProject
|
||||||
|
metadata:
|
||||||
|
name: homelab
|
||||||
|
namespace: argocd
|
||||||
|
spec:
|
||||||
|
description: Homelab GitOps — single-repo, in-cluster destinations only
|
||||||
|
sourceRepos:
|
||||||
|
- https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
# Public Helm chart repos referenced by k8s/argocd/apps/* and bootstrap/*
|
||||||
|
- https://cloudnative-pg.github.io/charts
|
||||||
|
- https://dl.gitea.com/charts/
|
||||||
|
- https://charts.min.io/
|
||||||
|
- https://operator.min.io/
|
||||||
|
- https://prometheus-community.github.io/helm-charts
|
||||||
|
- https://grafana.github.io/helm-charts
|
||||||
|
- https://helm.releases.hashicorp.com
|
||||||
|
- https://charts.goauthentik.io
|
||||||
|
- https://strimzi.io/charts/
|
||||||
|
- https://charts.bitnami.com/bitnami
|
||||||
|
- https://homarr-labs.github.io/charts
|
||||||
|
- https://go.temporal.io/helm-charts
|
||||||
|
- https://portainer.github.io/k8s/
|
||||||
|
# Substrate charts (cert-manager, ingress-nginx, reloader) — app-of-apps owned
|
||||||
|
- https://charts.jetstack.io
|
||||||
|
- https://kubernetes.github.io/ingress-nginx
|
||||||
|
- https://stakater.github.io/stakater-charts
|
||||||
|
destinations:
|
||||||
|
- server: https://kubernetes.default.svc
|
||||||
|
namespace: "*"
|
||||||
|
clusterResourceWhitelist:
|
||||||
|
- group: "*"
|
||||||
|
kind: "*"
|
||||||
|
namespaceResourceWhitelist:
|
||||||
|
- group: "*"
|
||||||
|
kind: "*"
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Phase 1 entry point — the app-of-apps root.
|
||||||
|
# Apply once (kubectl apply -k k8s/argocd/root) then sync it; it renders every
|
||||||
|
# Application manifest under k8s/argocd/apps/ and those deploy the whole cluster
|
||||||
|
# in sync-wave order. Requires Forgejo to be serving the repo (Phase 0 first).
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: homelab-root
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "0"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: https://github.com/Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/argocd/apps
|
||||||
|
directory:
|
||||||
|
recurse: false
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: argocd
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
metadata:
|
||||||
|
name: homelab-root
|
||||||
|
|
||||||
|
# Phase 1 app-of-apps entry point.
|
||||||
|
# `kubectl apply -k k8s/argocd/root` creates the homelab-root Application, which
|
||||||
|
# then renders every child under k8s/argocd/apps/ and deploys the cluster.
|
||||||
|
resources:
|
||||||
|
- homelab-root.yaml
|
||||||
@@ -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:FvlbdMkcngJchi0GEjIEDjxXpQjRwh+3xnlZNpApgd3v1SpMd0qD2d3TbQr39+feSfsvpxhdmmyU/PmRHZYVdr/QpIKlZBWBD6qTCaOSq62NQbR4ck336muIaWD4AmahWCvRouYEJdEsnG7nedjAivw4cls18vMfnL5pNm2t96foScSyHosWoh3RdT6CWLmJE39+kS6/fwZ1D3Z/GEm/E2zHkVtEbEw/fk1PiqsnmZWt8QcSRAeX8aIDkphV7wOKlkri11r1NuFQKMVIau07VPce1YEi5rsJqsircvDxQelQiMGQt2y2M4GGlXx2NeJgKEtP4Hf9mVGhTeN3CYLcXi8Qyg7k1GIawGVW/Em0kWWfy0GMjvOMfYozNmIpi0YQdYBbr3j6pkixtVM2dWezvld5QIYLnHoGMxw4M6L4IqVIonC3j6tk9pV1DaL5IFskEXyb6ScTrmVY4mQ+VEXvfLYA38jRauWxx/4qf4I7B0RzfGzOx67UJh2jKj0bPWNFHKt5Dz4CK+ul39d1KUPHHMoyaH5gCpOAfLSkjMm44X1Cn+O6uY+QFCugh9MI7bJg3EbovTCZxz95GCupWAvDshv59f7gpGL0T8AuRZHlNp6yZMLdviR9yf79d2rW26PIM1QuNOUWELrOcTs5IAmyuFYr0PI5W5XSf4klOdz3UdhPfMDcyRqtSzVDsSo9oC/WeWOo1+yurtdqqW0wLiFpU/kTaZ4JN5Kl080twqFEqgyPnJrbmlxwWlt4XN2SV9D7pcfE61FzUTwoi5PD/8xt94Fp3XerSgJtwhQ6X4Neo47wCFoMR4Y69mlHrFgSJA4kLFezDc2ISnyAix0W5to0cjwJqQ2EgonIhCXD4xWTvYeApHJMOC2o4B74K5efOZ7klb6PwDMvzM3LyeGzjoJh47aJGvhWN/MpQZC2lSHT8PdMxdGDc+OkEVmsvMncddWMGjQhkTo+69sUexARxLY4H22TCt58azqQGcWa5e7s9NUHb6bRVKHzXh4HMiIhQ0jBev++Jz5gIOyYRnw5xoGbo3ROv8ndczVAqXyAla23EM/VBWT0hLMjr6xxonuLQacP9dPJE9QUofjRMeYblkVvQdIw8WuKGbAlbTR/bokQwGdVp2bywSFQZj7BLlPjzCr0/LJmxYMdq6CKB049PdTlS9/UKBHApddMQ/QRXfOYUCX+lm4k4J19jd7dWk34WNUrTEPSR2d1U/2wTnMvzlrnQHx/zk4/lmI1newfjIbybIbXypX8xVEqzFWXpxUe+iFLP19KmmmHG7gGsV32k1VSGvYFI0HOokZga98QsAbJGbi1gQq8gYpMR/mBdhjJVcnAjOYpg+YaR0vCyrhBN55aSCDoUbT1JOqxvFqmlBHDxTWorD/3KGFxEH/wvSsxussFmUCWdEgYmjGO1ktnQBw1YpOB7mhDUlzvYCSxfpexsYufDYqTbrb/R5gmATC0pEN6EgTth5YqFOL7AiIsogy14RGjRym1wvjVl5SUU9bMQlWGJbQuvNRRwL/TzR/QP210jMsnAh9t8sncDnFPIbhFgeBLsU/ctYoBs0nFVWGWzcuoP9HTnvdWTT2xKmWAeFz8I3DZ5sjm2B787Q4bCrAszTvVLeLiRw/pOE32BeNfG4nNiZDvMQhUomAKpaQypWTu2wLsv3ISm00gud5sRKB6ASJANuSu+EhG9k4OYcBEzAT9PKUxclupFbmsFnXx0u9CXDj3tErrPCUs7DuFSr+fz0ehCLMH+GrcqfytjNDVoTSBRZg+lJYKjAqVCGvq8uRf5V/rdulbt3ggx/uhGSiPzYrBQ0g9iMdjQqpNGBwjlxGkhRE9D2n/PoerAAnHTfwc9Hezffncostjp09xw2CB/xOx9HZFGalQ1R4F5+0lm0iR4lMzc+se+o38foNIiMeMIwAVBJu7IO++UMuKCxpxmdAGqEYj/Ddpzfi3ZcRwe/1HhanVvKRMJsPOhnv0SB2xBrdb7QHnEzLbRTriVRBBdOI77HXWgDt9uQru0tEXCEQ0n+f0BWqHVa/RLN4Yj0dQVWLXU75ddyHnRaCiVdLQnObMvG/QXqY1k/bJTzGNgVF8/KGY51NgdfqskOXC9Fv5IGiyWq+/DL7tQcrmThkzUhZlwDBmwUzPFmzuUC7Z5PkXv2EV+eUrYF16HYqp5KgqTkBHr/JXR8ljb/5gMN929Jk2fBVEuzaEG6W9Yek+Qbx1cnXYrMBI9GWJUJsMsxsAj+1bAjtQeQwt0nXgZesTaV90dfQYq+nTGXMNjqaMhsrJhR6lU1+CDpwY+IObHSttS8AKB+Czsl0nUB44o1a/zKRDNTCGGmPFuv1Q5Ub/1myxorLDDF1mYDpre5TedygtZ/1YSz/YOpKwmWdW32FsnY+m3qjEAcfV5f923N/z8ENVxVpeiWL6mqf7h6F3qTiaSsFq2O8m8tVm70X+oRdig3hPnKqlK/KoxVmzZErhEJ+KmP5I2uvIlb4PViE6imu8kw4VqMClb7jJ7Yk+2Zy84C8RGuDCN8GCDG7FvAahA9xVGxB6rmEpzvqFBxos2EeLOlyfewf7rCw1JAL3C9hZrciA9Jqb/t8B7KqCmCOsKK7xVt1yqyG1dsa364JG/1SKSMKebTheuymZ1+VnGjRZ+pgSAkQGjCtp7B3fTlhjt+WwxW4Ml27yfjokQQ==,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:81dGm7VocQslAh1cLs8obpgDeCEoNPHcTkOPpgL2iHDz080+CEeCT/6mVyosvnZZ5hNLf2Zn/gD01nyvbc6Po7wRmP6yb9+MxusYLsbRy76NvCelyVe/re0VD3NrJw3RH9zHANtF41lI88tqqWrwYkea7456tLDSAGa4DgXOFyfNWBjomNU1iAQAf4IBMJ4zrSCbdWGRs2wV+8eaf1CRI5A3pJMujC9dvqNn8Zlm/dsHXibl0FBbRAYBwm0nr0UCyWn6nJH4YBuBPbAAcNwvbaxbvJ7BBHaObQASxTZhwLXASUJSTT3zIljV+4PkxHJ66gTtJgegnX6Mpk8PzRnH02I5ieETCTBpRdEjasFNS4AAO1Y2F4JWtElCnK8N0ut/nLQKiq5NKyns6ZE+t1v4V+1JrZAnpktPsKhe8+LRlibFU+g+sacG1lXAgJhR+ILZ1J/M4nfXrFStAHOwaxv/LXJk5mOwBfJ3SAq9GSuGQTiM2ByAgZMcgR+92KRUe7zl6yvzSL+QGkC1psS8e9+0KInNlH8TbW86bQFMOv8QNJPNBvu/7Ock1dBK7BX9fLD6Y9YslNgQ66KJUbOkNS3MdIegy4/DLXdkjklbBgVIE8nKiEanfzpY/CnnH+NCkZNISzbgjs4oxtaOyYSyE+JF+1NMgOHbf1eypf9KMJxIDDFCRRW0yy8k5nX5wOVh159o1Wp+jeasBltXsThNrXDFB+mzXd5Oid57gDeoI8tk8rMPqumVaxULs0eK86Rchu8Yv9QD4BWBG30SKSxiVrcDYldPe1tGhUvYk+aJhUbEOcj/8dn35vniohKXO4kC7B3a9aFkGTGG+8LD0xp07xUvjNLYZ77Bk2M+Rj4kASnPEG6KGXWE812MKwkqoMdsOrchS8FO2PK+VTBhMtW7X9yxymUZv64OzgBm9JPk1Qx048iqxzl0lfZlxy3B522uhmTVJIuwEcseU7Nk6liFcS9kRFAgJ23jTM4QXObMgI6GMPLge7JKdRUccGPJaQA7+nr771pnnFBTmlvI5p3qYs5nFW0RZqLt+qnSWcw5EA/7Py5B/VfIpyRkXMkzxD9I3DgX4XKNIuxF4N2HYkbIIoXEJSqAta90HtoLGxaOXzHA4urbGTuineZqbPI1j4DoN714QQePCwhEMSqKR0ajYQytznJd3bo9nEQnteYcRoFw8F1SbLzTZSjtPCPl6HHD2Uaxv+e9iw7LUKaoPBvOqRXLn2V3tC2rs68aFpPuAy7TwVhv8U0qIZWg0TbtA2YsEsN5TRrIMrkF6zQbfNExJYmuLDUhIbxxGlbXPOcVKWn7v4gGXEbRsvmSsV5y1GW88w1tkorQA1J6+OgrhgVwLXrg0i+G4jQEYnd2lnUq8AIbC13P9/iy8yalsB3CQTmPNmZf/xqtosJgXz3+d29MSMgj6yc6DjMxsvzFL2obNWhnSNVH2703Dp1MeYwN+pXavp+BCamUeQZYAkZaZID3h7D/Jssg9APTU6b/aYBXPacI41eyF5W1YWBEzNnCTaZp2n5IB1B+6ny6WaSyYm6AcFUy/LtpPCFWgbFryZQfUfEwGH6BPfU6Hrtsrla+AmFKqVwrs2QBYiQ/atoRFztDImvrmGy4hzS3pkRyh9K+stggCAoPQO1Vr9F4wK0jmGAaIEfqP3gomxrUVJqtAp6e8XmUGLLkoeRBh4yct7zflccrAVCUui1woQSApyceh7wif0TBK6yDF546MOzHChVy7t+58ssVsl/XEGjbWgvegjlhhO3vcSJmZq/DEN5fZiVJ1G9COUZ9i0MRB/p4ClYWwPVEMdAAczoGfLGDr1iYAF6BIiyBluojEH3fSK43y+4uqJJ349iP6JBYCmu8UG/x+RWMG85p9dt75vxsdbcb350RuNRmUgso2EsyoDuMNjmPQIbjn+MAGj3x3Qur2sKYsQ2nE/P7mD9tfdNZH2JYyuuLkkGkw3R/1UuqPmTZMUYJZKrpWjVJC6ZJUhsG4fI0BlHvjBOBL+OVbFBu8rVshhUTXjiDx1qvHdlAQ6Oq3CFfWFf0KbFcevqVYYI8vwI9mVSBdSbz00qm1ld6mIcWFd7q2eBEMYb4eimfZBjQSFKHaYc3fi79IjmGbjxLKI7Y3VSnbqVZHxWLHk9pKelxLIb2JSXN3Drj1MSadPEd9zjz4vxtWZ0VY9i5UGLg3xJjthbm5DlfYXZVV8YvdaC7H4nmpglXAQMVgnrxZmhYXqV3ywC4iywlUF0ug5VlJ/0s5Zp0I9rxEWiGjqdJ6Z8qeXL2w/bgYMVRlJ8RDPb+GIDAXAvO8cTOI9P/5R1tDj8s5/CYufNPVgnzUtggX2TMlXl7lknQ+HYPrbk3BUvL/n+ozubL3ATnoVVEZtREOUoxBVAT+q5q1bN4oagNOw==,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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user