Compare commits
271
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fb1c6feeb | ||
|
|
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,247 @@
|
|||||||
|
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 ==="
|
||||||
|
SECRETS_FOUND=0
|
||||||
|
|
||||||
|
for pattern in "password:" "secret:" "token:" "api_key:" "apikey:" "private_key:" "privatekey:"; do
|
||||||
|
if grep -r "$pattern" k8s/ --include="*.yaml" --include="*.yml" | grep -v "^Binary"; then
|
||||||
|
echo "⚠️ Found potential secret pattern: $pattern"
|
||||||
|
SECRETS_FOUND=$((SECRETS_FOUND + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $SECRETS_FOUND -gt 0 ]; then
|
||||||
|
echo "⚠️ Warning: Found $SECRETS_FOUND potential secrets"
|
||||||
|
echo "Secrets should be encrypted with SOPS or stored in ArgoCD Sealed Secrets"
|
||||||
|
else
|
||||||
|
echo "✓ No hardcoded secrets found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === 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"
|
||||||
+22
-3
@@ -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,22 @@ 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).
|
||||||
|
|
||||||
skills-lock.json
|
# Terraform state and cache (local files, remote state in MinIO)
|
||||||
|
.terraform/
|
||||||
|
terraform/.terraform/
|
||||||
|
terraform/*.tfstate
|
||||||
|
terraform/*.tfstate.*
|
||||||
|
terraform.tfvars.local
|
||||||
|
skills-lock.json
|
||||||
|
secrets-plaintext.yaml
|
||||||
|
skills-lock.json
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
CLAUDE.md
|
||||||
|
docs/
|
||||||
|
|
||||||
|
k8s/argocd/seed-repo-secret.yamlbootstrap-argocd.log
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
creation_rules:
|
||||||
|
- 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
+274
@@ -0,0 +1,274 @@
|
|||||||
|
#!/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)
|
||||||
|
# - GitHub read-only deploy key private half at $DEPLOY_KEY (public half added
|
||||||
|
# to the GitHub repo's Deploy keys)
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BOOT="$SCRIPT_DIR/k8s/bootstrap"
|
||||||
|
SOPS_KEY="${SOPS_KEY:-$HOME/.sops/key.txt}"
|
||||||
|
DEPLOY_KEY="${DEPLOY_KEY:-$HOME/.ssh/argocd_seed}"
|
||||||
|
GITHUB_SSH="[email protected]:Riotpiaole/riotpiao.homelab.com.git"
|
||||||
|
|
||||||
|
log() { echo "[$(date +%H:%M:%S)] $*"; }
|
||||||
|
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"
|
||||||
|
[[ -f "$DEPLOY_KEY" ]] || die "GitHub deploy key missing at $DEPLOY_KEY (see phase4-argocd/seed-repo-secret.example.yaml)"
|
||||||
|
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 + repository secret (idempotent)
|
||||||
|
kubectl create ns argocd --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
kubectl -n argocd create secret generic seed-github-repo \
|
||||||
|
--from-literal=type=git --from-literal=url="$GITHUB_SSH" \
|
||||||
|
--from-file=sshPrivateKey="$DEPLOY_KEY" --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
kubectl -n argocd label secret seed-github-repo argocd.argoproj.io/secret-type=repository --overwrite 2>/dev/null || true
|
||||||
|
|
||||||
|
# Decrypt and apply any encrypted secrets from bootstrap dir (local SOPS)
|
||||||
|
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,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,6 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
# No namespace: RuntimeClass is cluster-scoped.
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- runtimeclass.yaml
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Namespace for GPU node-level plumbing (device plugin, and later DCGM).
|
||||||
|
#
|
||||||
|
# PodSecurity must be `privileged` here. The cluster default from the Talos
|
||||||
|
# controlplane config is `enforce: baseline` with exemptions only for
|
||||||
|
# kube-system, and a device plugin cannot satisfy baseline: it has to mount the
|
||||||
|
# kubelet device-plugin socket and the CDI/driver directories as hostPath
|
||||||
|
# volumes, which baseline forbids outright:
|
||||||
|
#
|
||||||
|
# Error creating: pods "nvidia-device-plugin-xxxxx" is forbidden:
|
||||||
|
# violates PodSecurity "baseline:latest": hostPath volumes
|
||||||
|
# (volumes "kubelet-device-plugins-dir", "mps-root", "mps-shm", "cdi-root")
|
||||||
|
#
|
||||||
|
# This is inherent to how device plugins work, not a workaround. Scope is
|
||||||
|
# limited to this namespace; the engine namespace (llm-serving) stays on the
|
||||||
|
# cluster default.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: gpu-system
|
||||||
|
labels:
|
||||||
|
pod-security.kubernetes.io/enforce: privileged
|
||||||
|
pod-security.kubernetes.io/audit: privileged
|
||||||
|
pod-security.kubernetes.io/warn: privileged
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Cluster-scoped prerequisite for every GPU workload on worker-1.
|
||||||
|
#
|
||||||
|
# The Talos nvidia-container-toolkit extension already registers the containerd
|
||||||
|
# runtime handler (/etc/cri/conf.d/10-nvidia-container-runtime.part ->
|
||||||
|
# plugins."io.containerd.cri.v1.runtime".containerd.runtimes.nvidia), but the
|
||||||
|
# Kubernetes RuntimeClass object is separate and is NOT created by the
|
||||||
|
# extension. Without it every pod carrying runtimeClassName: nvidia is rejected
|
||||||
|
# at admission with:
|
||||||
|
# pods "..." is forbidden: pod rejected: RuntimeClass "nvidia" not found
|
||||||
|
#
|
||||||
|
# Deliberately NOT setting nvidia as containerd's default_runtime_name (the
|
||||||
|
# 20-customization.part patch in the Talos guide): that would route every pod on
|
||||||
|
# the node through the NVIDIA runtime. Opting in per-pod is narrower.
|
||||||
|
apiVersion: node.k8s.io/v1
|
||||||
|
kind: RuntimeClass
|
||||||
|
metadata:
|
||||||
|
name: nvidia
|
||||||
|
handler: nvidia
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# 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_CLIENT_NAME: "Authentik"
|
||||||
|
AUTH_OIDC_GROUPS_ATTRIBUTE: "groups"
|
||||||
|
AUTH_OIDC_SCOPE_OVERWRITE: "openid email profile groups"
|
||||||
|
AUTH_OIDC_AUTO_LOGIN: "false"
|
||||||
|
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: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 512Mi
|
||||||
@@ -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,74 @@
|
|||||||
|
# Embeddings — Nomic Embed Text v2 (MoE), on CPU via HuggingFace TEI.
|
||||||
|
#
|
||||||
|
# CPU, not GPU, deliberately. All 4 V100s are claimed by the generation models,
|
||||||
|
# and the device plugin hands out WHOLE GPUs — a 5th GPU-requesting pod is
|
||||||
|
# unschedulable no matter how much VRAM is free. Sharing would need global
|
||||||
|
# time-slicing, which on a single node cannot be scoped to one card and would let
|
||||||
|
# the scheduler co-locate two ~20GB models and OOM both.
|
||||||
|
#
|
||||||
|
# worker-1 has 96 cores with ~250m requested, and this is a 475M-param encoder
|
||||||
|
# (305M active). Retrieval runs once per agent-loop iteration, not per token, so
|
||||||
|
# CPU latency here is immaterial. This is also what Plan 1 originally specified.
|
||||||
|
#
|
||||||
|
# TEI (not vLLM) because it is purpose-built for encoders and explicitly lists
|
||||||
|
# nomic-embed-text-v2-moe as supported.
|
||||||
|
#
|
||||||
|
# NOTE: Nomic v2 requires task prefixes on the CLIENT side —
|
||||||
|
# documents: "search_document: <text>"
|
||||||
|
# queries: "search_query: <text>"
|
||||||
|
# Embedding without the prefix silently degrades retrieval quality.
|
||||||
|
apiVersion: serving.kserve.io/v1beta1
|
||||||
|
kind: InferenceService
|
||||||
|
metadata:
|
||||||
|
name: embeddings
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: llm-embeddings
|
||||||
|
app.kubernetes.io/part-of: llm-serving
|
||||||
|
spec:
|
||||||
|
predictor:
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 1
|
||||||
|
# Pinned to worker-1 only so it can share the RWO models PVC with the GPU
|
||||||
|
# pods (RWO = single node, any number of pods on it).
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: worker-1
|
||||||
|
containers:
|
||||||
|
- name: kserve-container
|
||||||
|
image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.8.2@sha256:4d632b76bd14cb57044a1ffb0ad48ab0ba4939e705a9a615ccc740658575c26e
|
||||||
|
args:
|
||||||
|
- --model-id=nomic-ai/nomic-embed-text-v2-moe
|
||||||
|
- --port=8080
|
||||||
|
- --hostname=0.0.0.0
|
||||||
|
# Truncate rather than 413 on over-long input.
|
||||||
|
- --auto-truncate
|
||||||
|
env:
|
||||||
|
- name: HUGGINGFACE_HUB_CACHE
|
||||||
|
value: /mnt/models
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "8"
|
||||||
|
memory: 4Gi
|
||||||
|
limits:
|
||||||
|
cpu: "16"
|
||||||
|
memory: 8Gi
|
||||||
|
volumeMounts:
|
||||||
|
- name: models
|
||||||
|
mountPath: /mnt/models
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 60
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
periodSeconds: 10
|
||||||
|
volumes:
|
||||||
|
- name: models
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: llm-models
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Action engine — Ornith-1.0-35B on Ollama.
|
||||||
|
#
|
||||||
|
# Why not vLLM like the other two: Ornith is
|
||||||
|
# Qwen3_5MoeForConditionalGeneration (Qwen3.5 MoE, 256 experts / 8 active,
|
||||||
|
# hybrid attention — 30 linear_attention + 10 full_attention layers). vLLM's
|
||||||
|
# Qwen3.5 support landed 2026-07-29, AFTER vLLM dropped Volta (sm_70) at
|
||||||
|
# v0.11.1. No vLLM build has both, so Ornith cannot run on vLLM on a V100.
|
||||||
|
#
|
||||||
|
# Ollama ships `ornith:35b` in its library and runs a llama-server runner
|
||||||
|
# underneath, which keeps Volta support. q4 is ~21GB — fits one 32GB V100 with
|
||||||
|
# room for KV.
|
||||||
|
#
|
||||||
|
# OLLAMA_KEEP_ALIVE=-1 is load-bearing: the harness calls this every loop
|
||||||
|
# iteration, and Ollama's default is to evict an idle model after 5m, which
|
||||||
|
# would add a ~21GB reload to a random future request.
|
||||||
|
apiVersion: serving.kserve.io/v1beta1
|
||||||
|
kind: InferenceService
|
||||||
|
metadata:
|
||||||
|
name: ornith
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: llm-ornith
|
||||||
|
app.kubernetes.io/part-of: llm-serving
|
||||||
|
spec:
|
||||||
|
predictor:
|
||||||
|
minReplicas: 1
|
||||||
|
# Recreate, not the default RollingUpdate: GPUs are allocated exactly 4/4,
|
||||||
|
# so a surge pod has no card to claim and sits Pending while the old pod is
|
||||||
|
# never torn down — a deadlock. Recreate tears down first, accepting a brief
|
||||||
|
# gap during updates.
|
||||||
|
deploymentStrategy:
|
||||||
|
type: Recreate
|
||||||
|
maxReplicas: 1
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: worker-1
|
||||||
|
runtimeClassName: nvidia
|
||||||
|
containers:
|
||||||
|
- name: kserve-container
|
||||||
|
image: ollama/ollama:0.32.9@sha256:1685741456770df6e3cceb2a945a5f75e020f658d1701509668d6f4688f1dd3f
|
||||||
|
# `ollama serve` does not pull models, and `ollama pull` needs a running
|
||||||
|
# server — so background the server, wait for it, pull, then hand the
|
||||||
|
# foreground back to serve.
|
||||||
|
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
|
||||||
|
wait $SERVE_PID
|
||||||
|
env:
|
||||||
|
# Match the port the other two engines use.
|
||||||
|
- name: OLLAMA_HOST
|
||||||
|
value: "0.0.0.0:8080"
|
||||||
|
- name: OLLAMA_MODELS
|
||||||
|
value: /mnt/models/ollama
|
||||||
|
# Ollama defaults to a 4096 context, far too small for an agentic
|
||||||
|
# coding model. Ornith's hybrid attention means only 10 of its 40
|
||||||
|
# layers hold a conventional KV cache, so 32K is affordable in the
|
||||||
|
# ~11GiB left after its 21GB of weights.
|
||||||
|
- name: OLLAMA_CONTEXT_LENGTH
|
||||||
|
value: "32768"
|
||||||
|
# Never evict — this model is on the harness's hot path.
|
||||||
|
- name: OLLAMA_KEEP_ALIVE
|
||||||
|
value: "-1"
|
||||||
|
# Serial agent loop; no benefit from parallel slots.
|
||||||
|
- name: OLLAMA_NUM_PARALLEL
|
||||||
|
value: "1"
|
||||||
|
# 2, so ornith and the small utility model stay co-resident on GPU2
|
||||||
|
# instead of evicting one another on every alternating request.
|
||||||
|
- name: OLLAMA_MAX_LOADED_MODELS
|
||||||
|
value: "2"
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "8"
|
||||||
|
memory: 8Gi
|
||||||
|
nvidia.com/gpu: "1"
|
||||||
|
limits:
|
||||||
|
cpu: "16"
|
||||||
|
memory: 16Gi
|
||||||
|
nvidia.com/gpu: "1"
|
||||||
|
volumeMounts:
|
||||||
|
- name: models
|
||||||
|
mountPath: /mnt/models
|
||||||
|
# Probes must confirm the MODEL is present, not just that the server
|
||||||
|
# answers. Ollama's `GET /` returns 200 ("Ollama is running") the moment
|
||||||
|
# `ollama serve` binds — which is before the ~21GB pull finishes. An
|
||||||
|
# httpGet probe would therefore mark this pod Ready with no model
|
||||||
|
# loaded, and KServe would route traffic to it.
|
||||||
|
startupProbe:
|
||||||
|
exec:
|
||||||
|
command: ["/bin/sh", "-c", "ollama list 2>/dev/null | grep -q ornith && ollama list 2>/dev/null | grep -q qwen2.5"]
|
||||||
|
periodSeconds: 15
|
||||||
|
failureThreshold: 120
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["/bin/sh", "-c", "ollama list 2>/dev/null | grep -q ornith && ollama list 2>/dev/null | grep -q qwen2.5"]
|
||||||
|
periodSeconds: 10
|
||||||
|
volumes:
|
||||||
|
- name: models
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: llm-models
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# Reasoning engine — DeepSeek-R1-Distill-Qwen-32B, GPTQ INT4, vLLM.
|
||||||
|
#
|
||||||
|
# TP=1 with 2 data-parallel replicas (GPU0 + GPU1) rather than one TP=2 engine:
|
||||||
|
# worker-1 has NO NVLink, so tensor-parallel's per-token all-reduce would cross
|
||||||
|
# PCIe on every decode step. Two independent replicas need zero inter-GPU
|
||||||
|
# communication and KServe load-balances them behind one Service.
|
||||||
|
#
|
||||||
|
# vLLM is pinned to v0.11.0 — the LAST release that compiles sm_70 (Volta)
|
||||||
|
# kernels. v0.11.1 dropped 7.0 from CUDA_SUPPORTED_ARCHS. Do not bump this
|
||||||
|
# without re-checking CMakeLists.txt, or every pod dies with "no kernel image".
|
||||||
|
apiVersion: serving.kserve.io/v1beta1
|
||||||
|
kind: InferenceService
|
||||||
|
metadata:
|
||||||
|
name: reasoning
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: llm-reasoning
|
||||||
|
app.kubernetes.io/part-of: llm-serving
|
||||||
|
spec:
|
||||||
|
predictor:
|
||||||
|
minReplicas: 2
|
||||||
|
# Recreate, not the default RollingUpdate: GPUs are allocated exactly 4/4,
|
||||||
|
# so a surge pod has no card to claim and sits Pending while the old pod is
|
||||||
|
# never torn down — a deadlock. Recreate tears down first, accepting a brief
|
||||||
|
# gap during updates.
|
||||||
|
deploymentStrategy:
|
||||||
|
type: Recreate
|
||||||
|
maxReplicas: 2
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: worker-1
|
||||||
|
runtimeClassName: nvidia
|
||||||
|
containers:
|
||||||
|
- name: kserve-container
|
||||||
|
image: vllm/vllm-openai:v0.11.0@sha256:014a95f21c9edf6abe0aea6b07353f96baa4ec291c427bb1176dc7c93a85845c
|
||||||
|
args:
|
||||||
|
# GPTQ, NOT AWQ. vLLM hard-refuses AWQ below compute capability 75:
|
||||||
|
# "The quantization method awq is not supported for the current GPU.
|
||||||
|
# Minimum capability: 75. Current capability: 70."
|
||||||
|
# V100 is sm_70. GPTQ's min capability is 60, so it runs. (gptq_marlin
|
||||||
|
# needs 80 and fp8 needs 80 — both also out.) Same 19.3GB footprint.
|
||||||
|
# desc_act=False in this build: no activation reordering, faster.
|
||||||
|
- --model=unsloth/DeepSeek-R1-Distill-Qwen-32B-bnb-4bit
|
||||||
|
- --served-model-name=reasoning
|
||||||
|
# bitsandbytes nf4. GPTQ passed vLLM's min_capability=60 check but was
|
||||||
|
# numerically WRONG on sm_70 (garbage logits) — proven by the fp16
|
||||||
|
# control run producing correct text with the identical backend. bnb
|
||||||
|
# declares min_capability=70, but treat that as unverified until the
|
||||||
|
# output itself is checked.
|
||||||
|
# NOTE the repo sets bnb_4bit_compute_dtype=bfloat16, which Volta does
|
||||||
|
# not have; --dtype=float16 must override it.
|
||||||
|
- --quantization=bitsandbytes
|
||||||
|
# Volta has no bf16 — must be explicit, the repo's weights are bf16.
|
||||||
|
- --dtype=float16
|
||||||
|
# No FP8 KV on Volta; stays fp16.
|
||||||
|
- --kv-cache-dtype=auto
|
||||||
|
- --tensor-parallel-size=1
|
||||||
|
- --max-model-len=16384
|
||||||
|
# VRAM budget on a 32GiB V100: 0.92 => ~29.4GiB, minus ~18GiB of GPTQ
|
||||||
|
# weights leaves ~11GiB for KV + activations. One full 32K sequence
|
||||||
|
# costs 32768 x 256KB = 8GiB of KV, so 8 concurrent full-length
|
||||||
|
# sequences is not physically possible here — 4 is honest, and a
|
||||||
|
# serial single-user harness never needs more.
|
||||||
|
- --gpu-memory-utilization=0.90
|
||||||
|
- --max-num-seqs=4
|
||||||
|
# Smooths Volta's slow prefill (no FlashAttention2 on sm_70).
|
||||||
|
- --enable-chunked-prefill
|
||||||
|
- --enable-prefix-caching
|
||||||
|
# Splits <think>…</think> into its own channel.
|
||||||
|
- --reasoning-parser=deepseek_r1
|
||||||
|
- --host=0.0.0.0
|
||||||
|
- --port=8080
|
||||||
|
env:
|
||||||
|
# FlashAttention2 requires sm_80; Volta must fall back to xformers.
|
||||||
|
# flashinfer's check_cuda_arch() has an upstream bug that crashes on
|
||||||
|
# ANY sm_7x GPU: `elif major == 7 and minor.isdigit()` calls .isdigit()
|
||||||
|
# on an int, so instead of reporting "unsupported" it raises
|
||||||
|
# AttributeError: 'int' object has no attribute 'isdigit'
|
||||||
|
# and engine init dies. Default is None (auto-detect), which walks
|
||||||
|
# straight into that path. 0 disables the flashinfer sampler outright.
|
||||||
|
# Only affects the generate runner — the verifier (pooling) never hits
|
||||||
|
# the sampler, which is why it started fine and this did not.
|
||||||
|
- name: VLLM_USE_FLASHINFER_SAMPLER
|
||||||
|
value: "0"
|
||||||
|
# TRITON_ATTN, not XFORMERS. On sm_70 every xformers kernel is
|
||||||
|
# rejected for V1's paged-attention bias type:
|
||||||
|
# fa2F / triton_splitKF -> require sm_80
|
||||||
|
# cutlassF -> supports sm_70 but not
|
||||||
|
# PagedBlockDiagonalCausalWithOffsetPaddedKeysMask
|
||||||
|
# -> NotImplementedError kills EngineCore on the FIRST request, which
|
||||||
|
# takes the whole pod down (vLLM treats engine death as fatal).
|
||||||
|
# V0, whose hand-written paged kernels did support sm_70, was REMOVED
|
||||||
|
# in v0.11.0, so VLLM_USE_V1=0 has nothing to fall back to.
|
||||||
|
# Triton JIT-compiles for the local arch, so it is the last option.
|
||||||
|
- name: VLLM_ATTENTION_BACKEND
|
||||||
|
value: TRITON_ATTN
|
||||||
|
- name: HF_HOME
|
||||||
|
value: /mnt/models
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "8"
|
||||||
|
memory: 8Gi
|
||||||
|
nvidia.com/gpu: "1"
|
||||||
|
limits:
|
||||||
|
cpu: "16"
|
||||||
|
memory: 16Gi
|
||||||
|
nvidia.com/gpu: "1"
|
||||||
|
volumeMounts:
|
||||||
|
- name: models
|
||||||
|
mountPath: /mnt/models
|
||||||
|
- name: shm
|
||||||
|
mountPath: /dev/shm
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
# Cold start pulls ~18Gi of weights over Longhorn, then loads to VRAM.
|
||||||
|
periodSeconds: 15
|
||||||
|
failureThreshold: 80
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
periodSeconds: 10
|
||||||
|
volumes:
|
||||||
|
- name: models
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: llm-models
|
||||||
|
- name: shm
|
||||||
|
emptyDir:
|
||||||
|
medium: Memory
|
||||||
|
sizeLimit: 2Gi
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Reranker — BAAI/bge-reranker-base, on CPU via HuggingFace TEI.
|
||||||
|
#
|
||||||
|
# Second stage of retrieval: the embedding model fetches a coarse top-k by
|
||||||
|
# vector similarity, this cross-encoder re-scores those candidates against the
|
||||||
|
# query directly. That is what fixes the "semantic dilution" problem in Plan 1 —
|
||||||
|
# a single embedding vector cannot represent a large chunk faithfully, so
|
||||||
|
# ranking by cosine alone surfaces near-misses.
|
||||||
|
#
|
||||||
|
# CPU for the same reason as the embedding service: all 4 GPUs are claimed and
|
||||||
|
# the device plugin allocates whole cards. A 568M cross-encoder scoring ~20-50
|
||||||
|
# candidates per query is well within CPU budget.
|
||||||
|
#
|
||||||
|
# Arch is XLMRobertaForSequenceClassification, which TEI serves as /rerank.
|
||||||
|
# 278M params — smaller than v2-m3 (568M) and English/Chinese rather than
|
||||||
|
# multilingual, which suits code+docs retrieval and is faster on CPU.
|
||||||
|
apiVersion: serving.kserve.io/v1beta1
|
||||||
|
kind: InferenceService
|
||||||
|
metadata:
|
||||||
|
name: reranker
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: llm-reranker
|
||||||
|
app.kubernetes.io/part-of: llm-serving
|
||||||
|
spec:
|
||||||
|
predictor:
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 1
|
||||||
|
# Same worker-1 pin as the embedding service, to share the RWO models PVC.
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: worker-1
|
||||||
|
containers:
|
||||||
|
- name: kserve-container
|
||||||
|
image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.8.2@sha256:4d632b76bd14cb57044a1ffb0ad48ab0ba4939e705a9a615ccc740658575c26e
|
||||||
|
args:
|
||||||
|
# bge-reranker-base, NOT v2-m3. TEI's CPU image starts the ONNX
|
||||||
|
# Runtime backend and v2-m3 ships no ONNX files, so it dies with
|
||||||
|
# "Model ONNX files not found in the repository". This build does.
|
||||||
|
- --model-id=BAAI/bge-reranker-base
|
||||||
|
- --port=8080
|
||||||
|
- --hostname=0.0.0.0
|
||||||
|
- --auto-truncate
|
||||||
|
env:
|
||||||
|
- name: HUGGINGFACE_HUB_CACHE
|
||||||
|
value: /mnt/models
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "8"
|
||||||
|
memory: 4Gi
|
||||||
|
limits:
|
||||||
|
cpu: "16"
|
||||||
|
memory: 8Gi
|
||||||
|
volumeMounts:
|
||||||
|
- name: models
|
||||||
|
mountPath: /mnt/models
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 60
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
periodSeconds: 10
|
||||||
|
volumes:
|
||||||
|
- name: models
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: llm-models
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Verifier — Qwen2.5-Math-PRM-7B, step-level process reward model, vLLM.
|
||||||
|
#
|
||||||
|
# Model choice was constrained by vLLM v0.11.0's registry: its arch
|
||||||
|
# (Qwen2ForProcessRewardModel) is natively registered, whereas the smaller
|
||||||
|
# community PRMs are Qwen2ForTokenClassification / Qwen2ForPrmModel, neither of
|
||||||
|
# which v0.11.0 can load (ForTokenClassification is absent from
|
||||||
|
# _SUFFIX_TO_DEFAULTS, so it won't even auto-convert).
|
||||||
|
#
|
||||||
|
# --runner pooling, NOT --task reward: --task is [DEPRECATED] in v0.11.0.
|
||||||
|
# Scoring goes to /pooling, not /v1/completions — this is a reward model, it
|
||||||
|
# returns scores, not tokens.
|
||||||
|
#
|
||||||
|
# Gets a whole dedicated GPU despite only needing ~15Gi: it sits on the
|
||||||
|
# harness's critical path (every reasoning->action->verify iteration waits on
|
||||||
|
# it), so isolation from the generation engines' decode loops matters more than
|
||||||
|
# the idle VRAM.
|
||||||
|
apiVersion: serving.kserve.io/v1beta1
|
||||||
|
kind: InferenceService
|
||||||
|
metadata:
|
||||||
|
name: verifier
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: llm-verifier
|
||||||
|
app.kubernetes.io/part-of: llm-serving
|
||||||
|
spec:
|
||||||
|
predictor:
|
||||||
|
minReplicas: 1
|
||||||
|
# Recreate, not the default RollingUpdate: GPUs are allocated exactly 4/4,
|
||||||
|
# so a surge pod has no card to claim and sits Pending while the old pod is
|
||||||
|
# never torn down — a deadlock. Recreate tears down first, accepting a brief
|
||||||
|
# gap during updates.
|
||||||
|
deploymentStrategy:
|
||||||
|
type: Recreate
|
||||||
|
maxReplicas: 1
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/hostname: worker-1
|
||||||
|
runtimeClassName: nvidia
|
||||||
|
containers:
|
||||||
|
- name: kserve-container
|
||||||
|
image: vllm/vllm-openai:v0.11.0@sha256:014a95f21c9edf6abe0aea6b07353f96baa4ec291c427bb1176dc7c93a85845c
|
||||||
|
args:
|
||||||
|
- --model=Qwen/Qwen2.5-Math-PRM-7B
|
||||||
|
- --served-model-name=verifier
|
||||||
|
# Pooling runner => reward scoring. Weights are bf16; Volta needs fp16.
|
||||||
|
- --runner=pooling
|
||||||
|
- --dtype=float16
|
||||||
|
- --tensor-parallel-size=1
|
||||||
|
- --max-model-len=4096
|
||||||
|
- --max-num-seqs=8
|
||||||
|
- --host=0.0.0.0
|
||||||
|
- --port=8080
|
||||||
|
env:
|
||||||
|
# flashinfer's check_cuda_arch() has an upstream bug that crashes on
|
||||||
|
# ANY sm_7x GPU: `elif major == 7 and minor.isdigit()` calls .isdigit()
|
||||||
|
# on an int, so instead of reporting "unsupported" it raises
|
||||||
|
# AttributeError: 'int' object has no attribute 'isdigit'
|
||||||
|
# and engine init dies. Default is None (auto-detect), which walks
|
||||||
|
# straight into that path. 0 disables the flashinfer sampler outright.
|
||||||
|
# Only affects the generate runner — the verifier (pooling) never hits
|
||||||
|
# the sampler, which is why it started fine and this did not.
|
||||||
|
- name: VLLM_USE_FLASHINFER_SAMPLER
|
||||||
|
value: "0"
|
||||||
|
- name: VLLM_ATTENTION_BACKEND
|
||||||
|
value: XFORMERS
|
||||||
|
- name: HF_HOME
|
||||||
|
value: /mnt/models
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "4"
|
||||||
|
memory: 8Gi
|
||||||
|
nvidia.com/gpu: "1"
|
||||||
|
limits:
|
||||||
|
cpu: "16"
|
||||||
|
memory: 16Gi
|
||||||
|
nvidia.com/gpu: "1"
|
||||||
|
volumeMounts:
|
||||||
|
- name: models
|
||||||
|
mountPath: /mnt/models
|
||||||
|
- name: shm
|
||||||
|
mountPath: /dev/shm
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
periodSeconds: 15
|
||||||
|
failureThreshold: 60
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
periodSeconds: 10
|
||||||
|
volumes:
|
||||||
|
- name: models
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: llm-models
|
||||||
|
- name: shm
|
||||||
|
emptyDir:
|
||||||
|
medium: Memory
|
||||||
|
sizeLimit: 1Gi
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: llm-serving
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- storageclass.yaml
|
||||||
|
- pvc-models.yaml
|
||||||
|
- inferenceservice-reasoning.yaml
|
||||||
|
- inferenceservice-ornith.yaml
|
||||||
|
- inferenceservice-verifier.yaml
|
||||||
|
- inferenceservice-embeddings.yaml
|
||||||
|
- inferenceservice-reranker.yaml
|
||||||
|
- networkpolicy.yaml
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: llm-serving
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Default-deny ingress for the serving pods.
|
||||||
|
#
|
||||||
|
# This is a real compensating control, not hygiene: vLLM is pinned to v0.11.0
|
||||||
|
# (forced — last release with Volta kernels), which sits below the patch line on
|
||||||
|
# several advisories that will never be backported to that branch, incl.
|
||||||
|
# CVE-2026-54234 (remote DoS) and GHSA-7m6h-x95x-82q5 (cross-user data leak).
|
||||||
|
# Those are all remote/unauthenticated attack surface, so keeping the engines
|
||||||
|
# reachable only from opted-in in-cluster clients is what keeps exposure low.
|
||||||
|
#
|
||||||
|
# Consumers opt in with label `llm-client: "true"`. Do NOT expose these via
|
||||||
|
# Ingress.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: llm-serving-default-deny
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/part-of: llm-serving
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
# Any pod, any namespace, that explicitly opts in as an LLM client.
|
||||||
|
- namespaceSelector: {}
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
llm-client: "true"
|
||||||
|
# Sibling engines (harness may chain calls between them).
|
||||||
|
- podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/part-of: llm-serving
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 8080
|
||||||
|
- from:
|
||||||
|
# Prometheus scraping /metrics.
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: monitoring
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 8080
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Shared HuggingFace cache for all three engines.
|
||||||
|
#
|
||||||
|
# ReadWriteOnce is correct here despite six pods mounting it: RWO means "one
|
||||||
|
# NODE", and every pod in this app is pinned to worker-1 via nodeSelector, so
|
||||||
|
# they share the volume legally. If a pod is ever allowed onto another node,
|
||||||
|
# this must become RWX first.
|
||||||
|
#
|
||||||
|
# StorageClass is longhorn-llm-local (1 replica, strict-local, disk tag `llm`)
|
||||||
|
# — NOT the default 3-replica class, which could not place this volume at all:
|
||||||
|
# every control-plane disk was already at its over-provisioning ceiling.
|
||||||
|
#
|
||||||
|
# Sizing (measured, not estimated):
|
||||||
|
# reasoning GPTQ INT4 19.3 GB
|
||||||
|
# ornith:35b q4 (ollama) 21.0 GB
|
||||||
|
# verifier Qwen2.5-Math-PRM-7B fp16 15.3 GB
|
||||||
|
# nomic-embed-text-v2-moe (CPU) 1.9 GB
|
||||||
|
# bge-reranker-base (CPU) 1.1 GB
|
||||||
|
# ------------------------------------------
|
||||||
|
# total ~58.6 GB (+ HF temp during pulls)
|
||||||
|
# The two reasoning replicas share ONE on-disk copy; they differ only in which
|
||||||
|
# GPU they load it onto.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: llm-models
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: longhorn-llm-local
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 120Gi
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Dedicated StorageClass for model weights on worker-1's local NVMe.
|
||||||
|
#
|
||||||
|
# Why not the default `longhorn` class (3 replicas, network-attached):
|
||||||
|
#
|
||||||
|
# 1. numberOfReplicas: 1 — model weights are re-downloadable from HuggingFace.
|
||||||
|
# Replicating them 3x buys nothing; losing a replica costs a re-pull, not
|
||||||
|
# data. The repo's "never delete a PVC without replicas/backups" rule exists
|
||||||
|
# for irreplaceable data, which this is not.
|
||||||
|
#
|
||||||
|
# 2. dataLocality: strict-local — keeps the single replica on the SAME node as
|
||||||
|
# the pod. All engines are pinned to worker-1, so weights are read from its
|
||||||
|
# local 751GB NVMe instead of over the network from a control-plane node.
|
||||||
|
# Removes ~60GB of network reads on every cold start.
|
||||||
|
#
|
||||||
|
# 3. diskSelector: llm — restricts this class to disks tagged `llm`, i.e. only
|
||||||
|
# worker-1's disk. Equally important, worker-1's disk carries that tag so
|
||||||
|
# UNTAGGED volumes (any ordinary cluster PVC) will not land on it. Before
|
||||||
|
# tagging, worker-1 had been silently hosting a replica of cicd/runner-dind,
|
||||||
|
# consuming GPU-node storage for general cluster workloads.
|
||||||
|
#
|
||||||
|
# The default 3-replica class also physically could not place this volume: all
|
||||||
|
# three control-plane disks were already at their over-provisioning ceiling
|
||||||
|
# (storage-over-provisioning-percentage=100, 30% reserved), so a 120Gi x3
|
||||||
|
# request failed with ReplicaSchedulingFailure on every node.
|
||||||
|
apiVersion: storage.k8s.io/v1
|
||||||
|
kind: StorageClass
|
||||||
|
metadata:
|
||||||
|
name: longhorn-llm-local
|
||||||
|
provisioner: driver.longhorn.io
|
||||||
|
allowVolumeExpansion: true
|
||||||
|
reclaimPolicy: Delete
|
||||||
|
volumeBindingMode: Immediate
|
||||||
|
parameters:
|
||||||
|
numberOfReplicas: "1"
|
||||||
|
dataLocality: "strict-local"
|
||||||
|
diskSelector: "llm"
|
||||||
|
staleReplicaTimeout: "30"
|
||||||
|
fsType: "ext4"
|
||||||
@@ -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,45 @@
|
|||||||
|
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", "temporalworkers"]
|
||||||
|
verbs: ["get", "list", "watch", "update", "patch"]
|
||||||
|
- apiGroups: ["kmsvc.io"]
|
||||||
|
resources: ["queues/status", "temporalworkers/status"]
|
||||||
|
verbs: ["get", "update", "patch"]
|
||||||
|
- apiGroups: ["kmsvc.io"]
|
||||||
|
resources: ["queues/finalizers", "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,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,42 @@
|
|||||||
|
# Wave 0 — Nginx Ingress Controller
|
||||||
|
# Foundational infrastructure required for all ingress resources and ArgoCD UI access.
|
||||||
|
# Must be wave 0 to ensure LoadBalancer IP is available before other apps deploy.
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: ingress-nginx
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "0"
|
||||||
|
finalizers:
|
||||||
|
- resources-finalizer.argocd.argoproj.io
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
revisionHistoryLimit: 3
|
||||||
|
sources:
|
||||||
|
- repoURL: https://kubernetes.github.io/ingress-nginx
|
||||||
|
chart: ingress-nginx
|
||||||
|
targetRevision: "4.15.1"
|
||||||
|
helm:
|
||||||
|
valueFiles:
|
||||||
|
- $values/k8s/bootstrap/ingress/nginx-values.yaml
|
||||||
|
- repoURL: [email protected]:Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
ref: values
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: ingress-nginx
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
- ServerSideApply=true
|
||||||
|
retry:
|
||||||
|
limit: 3
|
||||||
|
backoff:
|
||||||
|
duration: 10s
|
||||||
|
factor: 2
|
||||||
|
maxDuration: 3m
|
||||||
@@ -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: [email protected]: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: [email protected]: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: [email protected]: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: [email protected]: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,26 @@
|
|||||||
|
# Wave 0 — networking policies layered on the Cilium CNI + CoreDNS that the
|
||||||
|
# cluster bootstrap already installed (substrate). These are raw manifests only.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: cilium-policy
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "0"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: [email protected]:Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/bootstrap/cilium
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: kube-system
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
# coredns-config Application removed: CoreDNS (incl. homelab hostname rewrites)
|
||||||
|
# is owned by Talos via an inlineManifest (terraform/files/coredns/Corefile).
|
||||||
|
# Managing the coredns ConfigMap from ArgoCD too would let the two reconcilers
|
||||||
|
# fight and revert the rewrites.
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
# 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: [email protected]: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: [email protected]: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: [email protected]:Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/infra/longhorn
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: longhorn-system
|
||||||
|
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: [email protected]: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: [email protected]: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: [email protected]: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: [email protected]: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: [email protected]: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: [email protected]: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: [email protected]: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: [email protected]: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: [email protected]: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: [email protected]: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: [email protected]: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: [email protected]: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: [email protected]: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: [email protected]: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: [email protected]: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,119 @@
|
|||||||
|
# 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: [email protected]: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: [email protected]: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: [email protected]: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
|
||||||
|
---
|
||||||
|
# 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: [email protected]:Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
ref: values
|
||||||
|
- repoURL: [email protected]: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,156 @@
|
|||||||
|
# Wave 9-11 — GPU serving stack on worker-1 (4x Tesla V100 32GB).
|
||||||
|
#
|
||||||
|
# Ordering matters: device plugin must expose nvidia.com/gpu and the KServe CRDs
|
||||||
|
# must exist before any InferenceService is applied, hence three waves.
|
||||||
|
#
|
||||||
|
# NOTE the chart versions below are v-PREFIXED (v0.15.2, not 0.15.2) — that is
|
||||||
|
# how the KServe OCI tags are published; the unprefixed form 404s.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: gpu-runtimeclass
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
# Wave 8: must precede the device plugin, whose DaemonSet sets
|
||||||
|
# runtimeClassName: nvidia and is rejected at admission if the
|
||||||
|
# RuntimeClass does not exist yet.
|
||||||
|
argocd.argoproj.io/sync-wave: "8"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: [email protected]:Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/apps/gpu-runtimeclass
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
# Cluster-scoped resource; namespace is only the app's default context.
|
||||||
|
namespace: gpu-system
|
||||||
|
syncPolicy:
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: nvidia-device-plugin
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "9"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: https://nvidia.github.io/k8s-device-plugin
|
||||||
|
chart: nvidia-device-plugin
|
||||||
|
targetRevision: "0.19.3"
|
||||||
|
helm:
|
||||||
|
values: |
|
||||||
|
# Driver + container toolkit are supplied by Talos system extensions
|
||||||
|
# baked into the installer image (nonfree-kmod-nvidia-lts /
|
||||||
|
# nvidia-container-toolkit-lts). This chart ONLY advertises the GPUs to
|
||||||
|
# the kubelet — it does not and must not install drivers.
|
||||||
|
runtimeClassName: nvidia
|
||||||
|
nodeSelector:
|
||||||
|
nvidia.com/gpu: "true"
|
||||||
|
# Drop the chart's default nodeAffinity. It requires one of three
|
||||||
|
# Node-Feature-Discovery labels (feature.node.kubernetes.io/pci-10de.present,
|
||||||
|
# .../cpu-model.vendor_id=NVIDIA, or nvidia.com/gpu.present). NFD is not
|
||||||
|
# installed and Talos sets nvidia.com/gpu (no ".present" suffix), so the
|
||||||
|
# affinity matches zero nodes and the DaemonSet silently reports
|
||||||
|
# desiredNumberScheduled=0 with no events. nodeSelector is the constraint.
|
||||||
|
affinity: null
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: gpu-system
|
||||||
|
syncPolicy:
|
||||||
|
# Manual sync for first bring-up: watch device-plugin -> KServe -> models
|
||||||
|
# come up in order, and avoid auto-deploying while worker-1 is cordoned.
|
||||||
|
# Switch to `automated: {prune: true, selfHeal: true}` once proven.
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: kserve-crd
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "9"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: oci://ghcr.io/kserve/charts
|
||||||
|
chart: kserve-crd
|
||||||
|
targetRevision: v0.15.2
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: kserve
|
||||||
|
syncPolicy:
|
||||||
|
# Manual sync for first bring-up: watch device-plugin -> KServe -> models
|
||||||
|
# come up in order, and avoid auto-deploying while worker-1 is cordoned.
|
||||||
|
# Switch to `automated: {prune: true, selfHeal: true}` once proven.
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
# InferenceService CRD exceeds the annotation size limit for
|
||||||
|
# client-side apply.
|
||||||
|
- ServerSideApply=true
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: kserve
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "10"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: oci://ghcr.io/kserve/charts
|
||||||
|
chart: kserve
|
||||||
|
targetRevision: v0.15.2
|
||||||
|
helm:
|
||||||
|
values: |
|
||||||
|
kserve:
|
||||||
|
controller:
|
||||||
|
# RawDeployment => plain Deployments/Services, no Knative, no Istio.
|
||||||
|
# v0.18 renames this mode "Standard"; do not bump without checking.
|
||||||
|
deploymentMode: RawDeployment
|
||||||
|
gateway:
|
||||||
|
ingressGateway:
|
||||||
|
# Route through the existing ingress-nginx, not Gateway API.
|
||||||
|
# NOTE the nesting: it is gateway.ingressGateway.enableGatewayApi,
|
||||||
|
# not gateway.enableGatewayApi — Helm silently ignores the wrong
|
||||||
|
# key rather than erroring.
|
||||||
|
enableGatewayApi: false
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: kserve
|
||||||
|
syncPolicy:
|
||||||
|
# Manual sync for first bring-up: watch device-plugin -> KServe -> models
|
||||||
|
# come up in order, and avoid auto-deploying while worker-1 is cordoned.
|
||||||
|
# Switch to `automated: {prune: true, selfHeal: true}` once proven.
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
- ServerSideApply=true
|
||||||
|
---
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: llm-serving
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "11"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
source:
|
||||||
|
repoURL: [email protected]:Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/apps/llm-serving
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: llm-serving
|
||||||
|
syncPolicy:
|
||||||
|
# Manual sync for first bring-up: watch device-plugin -> KServe -> models
|
||||||
|
# come up in order, and avoid auto-deploying while worker-1 is cordoned.
|
||||||
|
# Switch to `automated: {prune: true, selfHeal: true}` once proven.
|
||||||
|
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:
|
||||||
|
- [email protected]: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: [email protected]: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,25 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:Fc0=,iv:UFqMFeZTJZtFMoNeusjAoSI60U9StZS4IXw+n8JQCRM=,tag:uUvY4+jcjiSJHs0HDS3/7A==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:NUw86IEw,iv:gh4Vqn6uteYnYIFcXT/QVxRPKm+G+j2aIVuV71nZ4k4=,tag:nZA/x75Kx4+CoSx66BsvkQ==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:qex150pm0eGwYiiI05y9LUs=,iv:5Tk001T796c4HFikoh1ZVB6ierBfDqOUkFs5ilkr+V8=,tag:LiDAiCH7w6aRS41VKIlIKg==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:nI91,iv:c1hBWGMkVb+vg+MKNzty8CFfXxQabXEtT+EpiuQZNvo=,tag:BsGtJDbBadDdTa687+Sdag==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:3rXawlSD,iv:FUtLK4KP7v/SBF08ZcU6siIO1qfjlJirf2x5PyzAjuA=,tag:mSIQ+Y43+wDfZGRbQ6a0Og==,type:str]
|
||||||
|
stringData:
|
||||||
|
AUTHENTIK_SECRET_KEY: ENC[AES256_GCM,data:i5tVOWnN9a/cfhs6gckveRxUpiN7Ie227R/74kOIz6lQ5US2hkf03W5qSGCrzheL3DB0i8+d0iJYsApjn/wCiN7ndZZVFko3W5jDfVPybxc=,iv:djF1G9JHT5B7ziTokA/UuPpG+tG2LvFU4uMAR0x4M6o=,tag:vHOd7jLYkM8Evnr0QUVrCw==,type:str]
|
||||||
|
AUTHENTIK_BOOTSTRAP_PASSWORD: ENC[AES256_GCM,data:PjvYtH03CzQbTXEqt/Z18+u1zJdok7DLwDn51FUZXDA=,iv:pSwSm8hB0/Q8uOAcOZ1rTqq7BF782EglG19GGeghm1E=,tag:qGCQFWCrwsWBOiQ07ySMUQ==,type:str]
|
||||||
|
AUTHENTIK_BOOTSTRAP_TOKEN: ENC[AES256_GCM,data:1tnekGo0GIemJCMRenjN0yNeSiiMl/kky8ms84TsqBLaQ0lQzPxjq9qtgNJwoAsufxrznrkDVKjeOEHNYcA82A==,iv:NNFWSLMILoWccA9ihy1OArSS1X3OySB7PIHdXZdyzlg=,tag:t2w+KyReOXftTrU9pBQDnQ==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSZGRKWm9PTkNSOENtREgx
|
||||||
|
M2V3Z0xxOXRhRGVRclo3SzBKRkc0VmtCY0NnClJPSGJRcUlzWmNFeTZ1bmorNEV4
|
||||||
|
U3l3TzVlMTlrWkJnQzBtUTJtcUwrM3MKLS0tIEJrMWZrbllYYUZKN1V4MVMzcXAv
|
||||||
|
eUtheHlNMHpsTEJXZ1FTMFVxbWloa0kKLRkE1Du+4gdOLerOl9y0mZw+8fqfECQY
|
||||||
|
uP64Q+9BSuPYNiTLZvYDzmdoy+KTS7i7C9rf+iU96R/crbQLamV9Ew==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T20:37:07Z"
|
||||||
|
mac: ENC[AES256_GCM,data:b3pDx340d1iZxsPPXzACckoLBrW+XjX1qlGrWEEI30j2Ao0hewTRZJyHbdI97A6b/hqzBN/4tjqqkdqm8goNhUQweMLTw86X6sALuE0aLS9wK53qjqih8fQwQBbgLDtjFXtW26h+smIm7aB5ZSDvU2le44HDXuWd83sWtCavNq0=,iv:ZMDsoKpygFJrYpXy2dA0IlxAi0jt24rxF72eUzgQs7k=,tag:WWE7G7+c9JK6fF1U9V8igg==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:qhU=,iv:/1G8RSCBaiQ6msQEPPOO4LzlbYOzEbNvT9OwnY0E1JQ=,tag:xO1Nw12oNk6wFCmw0YldEA==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:U/sQsZyV,iv:KMW7WL9ZjTmUz3NOQRhjsuAH2EZFXGk1P4hhl7cp8c0=,tag:0WiWyaaT1hmmVk837dFQfg==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:tQIaIJGaBzzLqa9vjPVlyhEymNupmKl6yyhx,iv:1qXckUq4RTBZ06YNLXsuMW1UWRK9cgymDdVnmwLZles=,tag:J2e9k7fmTYhhUFzRfkNYuA==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:bWQZWAI3UY04rdp2,iv:YCv7jIXpkzTx9iu9ScwHme8Nc8iqJr1xn8yXK/OsHk8=,tag:Xq9mX/EUHIOClV/YUm2suw==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:lQUZvLVW,iv:iseIGnyGeVE76BORrjX/TnvrfzaRMOFrQ7oWT0jIS9M=,tag:JMGEqkIwlrB25m5E1hBH7g==,type:str]
|
||||||
|
stringData:
|
||||||
|
api-token: ENC[AES256_GCM,data:i2P1Qm062lGubNILbbpzX/AWga7s+RhZ3ODsue4oWKZ7xMHvhYaLUG3/t7YEOmu2qxvvdjQ=,iv:uFvlesWdZUbiuh8WTq1uLBRtuptV2tXxGJ6PVgu7vCI=,tag:PXpdMfGR5Yui/e3PGRUWZQ==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3OE5SY1Q4OFdsSkJ2Q0NK
|
||||||
|
TmJmdk1RUEZHbS9qa2hPQm1hRzg5UTFKMGpNClRNRlZ6a0hjdDU2UkRHaDJKbmdW
|
||||||
|
MlZhTFBubm1HcS9MZTltQnNOLzBmNUEKLS0tIDZaSko0VGZOSm9EcFZ2L3V6cHVU
|
||||||
|
RFJ1bFY4OGJYb3pnMTFTZ1c1NjdoazgKhwqR2KeygYCpTR8u+pmMYzjNj3XyuhKZ
|
||||||
|
EBXYOrD3nR4JSoULAUHnZto1HwVVf6/SaR9rbg5OunOlCkUdopLNig==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:loYaADf5M4WMJCruMwcBHuF+MtQGYXrYv+MTNjtYRuk0yx4DGoTh2xiHMIZF7S52OCUoX4bn9qKqKR5sZK+XOWxD2hHovf/guo94HDklRqJ7ifDRshTz5wIlav3Zl5GgxUdxea3MmebghPEEm0uHVnvKxVbKBEw5WivCJd8JOyM=,iv:rYFOxaz/Lb0loPNVmXL06BPAl8NzYHn+3Zeoh1YjB+g=,tag:N+qbLLR0YhqJ5xbgTVvLdw==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:Rec=,iv:oFWV5gLi/v/mrA+i6n1hDONVS/iLHTZDDro2l3fYxZw=,tag:zYpAitxXQNc3QcnV0XiLXA==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:LpaVYuc0,iv:Vcmp/GhUhLK2/e39lpAXrzcD9CYstKjEojTThW+tyL8=,tag:yH47tiZkMe7Y7TKW7G+cKg==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:qHpkh1x88s//GHx3,iv:8aS0Q+HsUdsoMvxfYrz+hU+0ceml2mx44tlwYJOWUDI=,tag:SijqQ++2IN1tYa9M3GS2YA==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:DYQF9g==,iv:q4XjDTdYp0b6dBaSXHTK86RZVCXHWV8I4tsq3ms5z38=,tag:25hy4cqSumcJajA3DK/7tg==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:LG+x1Pzr,iv:ULVYrO9iIoZZCQDRhVF8WaPx31MBj0kXUJpKKgkcbyg=,tag:muo6E5YPTppgK0Vc2yq95w==,type:str]
|
||||||
|
stringData:
|
||||||
|
token: ENC[AES256_GCM,data:Hwwo8E9H27nq2A1FTB+Bx3nlQzowMkqIl6ChiXot8zt4l4UDjHXZPQ==,iv:kFiwyo6QZxwAPTXT9L5uXHbLhfULuyUv0eNSUe6eHhA=,tag:bSRKlb/J49Y1dumCphU4iA==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3bXptZU9CSXJKMlRJM3lZ
|
||||||
|
bEI4L01Gb0dpTGR5S1FTeXV6OXRwNTJmY0NzCkYwbjBSaXdjWm9iMUNsV0JscXc5
|
||||||
|
cXFYNkFLOFlXb0QwSkgvQjZrdS9PSVkKLS0tIE44Nko1cnVrVEN1bWpJajdFMFFG
|
||||||
|
V05lUHFiSzl1akdCUUVRdkQ2eHZEdmsKYXymkNu6pvIN0DW+3NYc95igGhzmm7MG
|
||||||
|
hH8IRVLLl1b2cW//wQsqngEnm8+UBtv09Bd7fyvfWbnoXqVAsshCcA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T23:18:57Z"
|
||||||
|
mac: ENC[AES256_GCM,data:lnokC1HjpIk94/666+PCbkkN57Rk6oDVtl6hbaM928honuyl25AU/dEbDYmz7VnaaigES95T487p0LjL0duePCoiZIigs1EKWwcPmAnfAo7aOZq8ZaOZb582KdjkAjJWVq+Ie9QMQE+AsDeKUAcE6QFdyQIKQyJK0pqL9YdXhxA=,iv:8Wd1340UGur9ABo6oC20yCSFXWwnFiN7I7fa3dg5HWA=,tag:W/pswWyRfvq0OcPh8rqSdQ==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#ENC[AES256_GCM,data:/JYNMOHrdMY2jobx67MjbJ7Eg8kHrNrkJcU535NRYLvY5nnSryk+asK8nQ==,iv:lM1jBHIxFkQriZ6BjGRXAlymUs5nX4Kvt1WNxUeZlkU=,tag:4pttiLNnIagcN4jTZ7BTwQ==,type:comment]
|
||||||
|
apiVersion: ENC[AES256_GCM,data:oRQ=,iv:0TzPcIoozs2MXJNXkzgcVtjjBUgfOHaSXQZiD37fb+Q=,tag:I1jwXteT9m2Pvdxq8zNtzg==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:zSxoKLqf,iv:Mf2s3h8++Vxqb4JoymHXY4/WAknDZ2GGrVVtKK51JxI=,tag:MrBrvl/FjIzS/AVoIa1rBQ==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:zKg/8UiwWslwTcesAQ==,iv:jxbj7Qtv+DRbhzTdvtv+eJuTQPNIf497NZPYA6ld4s0=,tag:HihBcoDLi8j9PB+uC265Sw==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:885ZKA==,iv:4PfWZu5qVGXP3ZzRHMrh5N9dzJ3SoUPPo58ppcDTnpk=,tag:gvBc+uYOmERckrb4KG2Hkw==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:eqT421de,iv:EZHnf1h1L29G1HOBYBSBeydNe4nC8XiBOw8YEL3kxrY=,tag:ypD8X9pa/9dbyChAMSIS6g==,type:str]
|
||||||
|
stringData:
|
||||||
|
username: ENC[AES256_GCM,data:Oc6vLw==,iv:1+tLTAxrDitXJwCAEccaVQzc9I9lNRgT3FsxO2NPDDc=,tag:r8lNDb42iewK8Soo9gbPwg==,type:str]
|
||||||
|
password: ENC[AES256_GCM,data:Ckc4PBc9tqigmcXl97iFUIqcjPzuJivT0tSmGunIDszFLU3u4UqAnA==,iv:jAwDFvJfQ1GkeU/qpEVUAQ6cWqxYE8nrgs+/RouyUxg=,tag:mP6AoUJP0T78+O9krL/eCA==,type:str]
|
||||||
|
email: ENC[AES256_GCM,data:qt+Sj2rs6l+L4x+kLUSaxlrN,iv:mfkMj3u8W2ZX4N4IH39mZXfEp+xphS4shaJcFOD/LEE=,tag:dD1jpIvTvPNOTer5gsfUqA==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0S3hzMFVxK3M4WVZzYWZF
|
||||||
|
a3krRjcwZlMrNDZ6d0FnZWZpdXNFc2tIYnk0CkVzZ0JBbGREUU0wL3dtSTBFTTNL
|
||||||
|
YTMya0duY1dlTWVadHAwUTB4cUJlSzQKLS0tIGFrWm9VRURmKzh1SXJPb05mSEdt
|
||||||
|
NmVUSVk5SnJGOTRhdHFKclhPMi9tSUUKCNhKWvUOExgirvRg3KaeEE+mRUPI3epI
|
||||||
|
xIYaTsiHvffmjc5mbo2sD6H2/L0h1IkLb78FJQdnTG8zl6yyFzKlUw==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:q46TmZXbALBGfexqqVtLn1YGp0GfWDzq6SqtEC0OujrCExHUyGG6Tb7BtkQDKCl68vkq0HkEwt9tKopGBJhUyC7gPNkXKZ+YmEEEdY84C3ePyvwWCAQ24R6o4Rpg8QURHSf9JA5RmoJOu77OcVMHkOB9Lan2K2dQVb+79WDx5L0=,iv:uQE/b1A1ZTgSCnk+wvwbHo7eWRW/IiKrqG1wlq1aA0I=,tag:WWXtlUi0jagSl/gKTIflqw==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:rGU=,iv:w1yJeQvN0MPZlPx8VQZmC6zkRaHW4rtjjROea0Kx9X4=,tag:qgYgVUGMQ2BtN0RZ0nZYjQ==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:wp2QP1he,iv:C8c/jKh5pnBOtHqXsTEchGf9DACU/5hOJ0Mp/+Jk1kg=,tag:QUZNeguRXdi3JO6Ov6GMDg==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:jVsnAcFTlRiKxDoU,iv:bWEqo28kH3bxzfI3ipiY6dsqhIcl9wIxBjFu0mxVGLg=,tag:PVWTveI8xhvLxvXPldhimQ==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:ZHnVZyd3wA==,iv:3evUnFTQfRz/ZilUQIl/9dsCba3nsX6o/iQsGXMBfgM=,tag:+9kp+VRu1ksroDTbTGmGPA==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:bHX+Bu9o,iv:8qqNddsuw4YoHAuJfUcPIqjcqNySIh5Ln8OhsfZxuk8=,tag:P7QGUXo9Io2Y19YqlWv5sQ==,type:str]
|
||||||
|
stringData:
|
||||||
|
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: ENC[AES256_GCM,data:qW2SQSsGD0YupU2SMzRaYwkLRjXAwV1OJBnhATpCJrOHWbEytw6IphOmPPd3VZKfHncekA4agXi4K3rUPHIx0Q==,iv:NCeW/UfIl0RvHV39ubN2mDfFp7GTQNuFv8KiEF+7KOE=,tag:lisx4h4PqUfLwBX8ca9Siw==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrUkxLMEV3aEpILytic0Ux
|
||||||
|
V2VEV2EvcENJVnNNRUtBbGkwT0d6eUxYeEFvCjZPSkU2MG5qME5jZ01mRVJwTUFL
|
||||||
|
M3M0V3c3QXBoRml1Vm9NWW5JTlNRSG8KLS0tIEJnNlFmdFJXb1BPaVJzTmo1RDQv
|
||||||
|
VXZOVHRHUEZJRUZ4eGZGM1RxSURyWXcK3ZyYCPhRUvpvT/pjPQGJoLIwaktZY6Tg
|
||||||
|
LS0cIDUhjPFbeu7qUATOrat6vMUi0UREbXSFZ1KyAjNAwJZaUusPMA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:yUBBV7dLEPld9yJacn+YaOT8M1NJ2ploGIxV66aVOH5x4tDAsskcUQJlzApWCQmORuCuMKg6qfpGgLTojfD5msha+ufVVJXHl7ua+STar1/FPJXaHm+z7YyZdsD5j6f3AH8q18DuLy2DYLjXELmPBsp50g7MDJEm3xLhCUXygjQ=,iv:xl0OtA/2umz0YH5b+6QjlAByZWbvfGOmHqomziNjlkw=,tag:xwkQDZx19Lf6QSwU2zPYTQ==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:x1g=,iv:ImbG3BggYpU11BV84Kwt1NrwoSZIrsoiZovqPM2ziWQ=,tag:V5clyseKFF/jEUX/Z/6Vmw==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:9NYfGQuq,iv:6ICJnhTiMP0QFX2xQxqE59AkbtFXQ3UJwH/v/2KaQdw=,tag:wHeJCUeTz4CkqDE/3sDpJA==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:vvZxPCX3S+jdtrkSZA==,iv:Aw9oxB0x3eLODe/XVGKd5A4UdKoW1ChbRM7Un0kkwFQ=,tag:1EdRix+CoNztbtXILUlHJw==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:ZVI3KHMJ9w==,iv:mf2ySxEaRrsPLgkwUUjmJuzklo74t8UTeCsUean2nx0=,tag:tOloglgAJIDNT1xNr//L9A==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:A3/oiYOS,iv:OMjn8JxX8g/jJSsIL911FX4autWEQDk1UGzfnnfKSjQ=,tag:p/8Rc6o7gfxoX5Lv8lhWvA==,type:str]
|
||||||
|
stringData:
|
||||||
|
admin-user: ENC[AES256_GCM,data:rjpH7gQ=,iv:UwIdUTnI2soFYGjZz6xj8boacSMYHxTg0Bl1xFLllsU=,tag:h2nX82mM2iT3tZu9SV9R5g==,type:str]
|
||||||
|
admin-password: ENC[AES256_GCM,data:hN1bHBVi9SfVzZ1uWA==,iv:4V3wlpMMfakInQoqD6gxk+0A6lWCm3aPw31gpa0sKss=,tag:na4lAI7SzbWTUB0lQ4uATg==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3eVI0b0pIZC83Nm53dEwz
|
||||||
|
TlV0alJCL2duOEFkSlV0bXp4Vmp5V0pwWVZFClNGSDNFeVluV2R2NUpEcEdYc0t5
|
||||||
|
R0xsalVTd2F5c1EvOXJPdHJ3Rjc0N1kKLS0tIGg4c1RBVlpwdEFQd1l3Z0RQYTBY
|
||||||
|
N2t3MVRTdFlPS3BvY1FJQmZlVG5ZVmsKEZx9fdx+p9UpzLGhN8D58KlPAOtyLtVs
|
||||||
|
ldAPmMThTIWQ5GgJjLwmag2NlMyO8NmQY+dd4a//Grx/4bep9lhjTg==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:YHngoakearZDcLH7ACS6Nw2hcf19XvVkthLxoM7KrWIAsa+Izkr/kr3PGgfeuwmQL0lux2P+GPlvLE4VeovAyPOiEBVl2tQK9/pXameqynswRn5wnl/kUMPJblNBtPpKR3jHlKsSD/o1MtQofm2mFLsa0bJ+C9JomAIYMFMKpms=,iv:M20AveeOFJs9Jj0jECODihC/nxIe/rPBU5QjfSQvuvs=,tag:dIDq+yaz6u87a1BLQCw0eA==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:uiQ=,iv:vknDc16Zrz/lxS1WG3ksG4SHjhJSTDdklVxxwL8eylA=,tag:1srpVd6QTYQGGGYGXT8Cbw==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:K6EPYaNE,iv:VWzawfTDNlag+shjBqwSzZGcQQq/TGxeHDqd4fAh5Vc=,tag:ncPqMYjq/73oMzphWhuuFA==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:vaRNQHFYfdUz1hzuYO0JxQ==,iv:W7aUJID5mxoO1NQAcyPDYV1FGtBT8myKVMA3s2WgrPk=,tag:4Ef4GBtTR3dVanscvigRmw==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:HDlmfGwad37/,iv:JFPXxz9xp5vDJRcWy73SYGWDMyHKWQq9Bvy/NJTwTnU=,tag:2ldVXgzKn4xp1rZI/VYgUw==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:UgolDLIV,iv:O4himI90UVwx+OjKJXobu10Zg3a5IoQcjAORsdhaWpg=,tag:xltntRsamhQkPEoBNtMytw==,type:str]
|
||||||
|
stringData:
|
||||||
|
oidc-client-id: ENC[AES256_GCM,data:RLdxXY/a,iv:US/fBReK7u8WA4IS6TSzYiHQkte2CuVgugIMpA/pIlA=,tag:QKrHGQrzI9YpfNQD+ts68A==,type:str]
|
||||||
|
oidc-client-secret: ENC[AES256_GCM,data:SS+I92xEkOcJHTfWvZ1AA8hh1bFAtND5adZ6Uh73nIPiqAo958GSag==,iv:9HcmXWrGtflc6WWQe0n/pgB1Xveakofc3hvz5vuE29A=,tag:0emgeyM4GBftVKn/fx7XPg==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCWkR5V0ZYSTBSSU1OTWF1
|
||||||
|
TzFxdnZkUFFIcGJWZ1VUU21ZUnU2cUdob1drCkhVTm02dkZPZ2lNWktOcWZ2c0Vi
|
||||||
|
dG1PSmZBR2M2anA2Z09iYUU0SjVzTlEKLS0tIGF1eVBBUHdhVk9YSzJlQXVwd3RE
|
||||||
|
bUd3UTlwR1NiYjNlcGpweTBhdmk5b0UK8pwUoVCr1uBX9dQ7EGRgFMAPk/z8N9Fg
|
||||||
|
6GRUORFug5h7x5kjXK9sU0yTHjFSekwBi75GLng0IrLEGJxHVM5wVQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T21:07:30Z"
|
||||||
|
mac: ENC[AES256_GCM,data:NupjQWBJHMOo3/VX5n5nRx+RFT14N71XkrjXRC/VCP7i7H8yn3tcRdH9g14usgWunmqg2KKpGMUIZ9bsQ9Vds9sBvUKvLyRkfQnPwPh9+PFCF3ZZLPgzqcT19kg4I3QabmO+LPzQXsZWLKIqgF9/B60EVIgYyKhZmP7XblY5AVo=,iv:r0deq8+txE8TwzdtxOMaIAZbZc2fu+Tp4Jd2HZGbkhM=,tag:qMpZp6LcWjJHYMDp9ecBkA==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:FO4=,iv:kKIuJp4uGeX6vECsXk7w43F9S281jObQchmK8r1+kmA=,tag:x5LYd34dzNDLCma/78M4+w==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:o4RTN8SK,iv:uyOJWjDmHIPuBl9Wc1S/HxUabV2yytWr1bntHW68sTI=,tag:3On0SnQVJ/ICMMryQe+8JQ==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:a89dHmLdSeMHE7ZP0Q==,iv:OWJvJhFqJ7A0UGVJnZILzCJ/4VuVFib9sUDjXuwyqJQ=,tag:1XrIQn1GDh8RQ4vmQAu7+g==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:4wBVpZJgQGM9,iv:3Tn1u/2TP/+SnELu34PDf4Jauf2L4f8+MtgrPIlyKzk=,tag:L3qbKFBjm16g8KX5vUsK6A==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:mdrF9vUb,iv:7ukOgwXlnRf5mpJ/+6YXtyGRonnqehEgT4pJJIafw7c=,tag:90mt7jpNb3N80kCEi+R/rA==,type:str]
|
||||||
|
stringData:
|
||||||
|
db-encryption-key: ENC[AES256_GCM,data:NWJryAEd/eUJxbNiu05RzLF/l7GDxIQM7RQe3xKBz6XklgRAFwQxxjakcVd85pePUBaQYz3zttFglWhfJlWhkA==,iv:eUWbcs0B/mIheyAQ2+DyKgcA98CRKQGAVC3O0WIkfU8=,tag:be06X6AQqlohYlxuYoZiTg==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1Ym5yQ1pmWHZjVnYxbUVO
|
||||||
|
SkZ5dnI2OXR3cmZjZkM0K1ErMElLNktlK1JFCmpleFBrcXkwcDA4cGFuWWN6VDQv
|
||||||
|
dVV4NjB4MjEreENVQnZ2ejVLblRYOVUKLS0tIG9VbXJvUmQ3SzMzN3IrRDlac2sr
|
||||||
|
eGZNYXBoZFZDeisrVCtUMXVPaEpoNFkKjwhUU+Zsqv4Be+m558fET5UhF/2rO73J
|
||||||
|
AhGbJVaA7PaOCswPWiOLso26zD+MBB3VKHNdUFnenpoM16Nu4pOf5w==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T21:07:30Z"
|
||||||
|
mac: ENC[AES256_GCM,data:KrGQY/eOmihYCvQCbfcM6bZ6rfx4zKh62M8NM0VQr1//1dSHpOOh25dYGLuQeECnqLxSpGQNWzr2xjUYWs4NOdSEFGq6FL2iHE3LnqoBZspp/JFJYP6+z/8jSUojwKEW7OuZZdPN6gF2u+IPOeWbGftnjS7X+atoSkYag2ty2vk=,iv:IOPEgBizsxrVoK5oSdWmi0uCBUnReOEnMYXuwSBKTzA=,tag:9k/XQ/AE7wSFq5XMbY0FTA==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:4OY=,iv:0Lgui0+X2oTlgCX5HEIIihEsGJD0C2+pDIOTCNi4r+g=,tag:uL7vlreaHjcLJjV7e5I7PQ==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:x/ltkind,iv:KRBfS7COsTs/l3Fhyk5FvH7IVc+r/6M6FVOo+kPDe1Q=,tag:UuTbe213hDfK/WUtKS/doQ==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:LpOEecTJBuGy5PqNSFM=,iv:Q+DCLGtw3wihhdAgEbzYHyQhFTPWFyE2iJKl9mYaA2E=,tag:E2wrig4lRXGDvSCHec5clQ==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:6kMVCXaF68NO,iv:gnCCkV7GKXA2HWOOeNUGnpAycf8U7pl6F8Vfo6DhLR4=,tag:0VdeySsuhUr2heXyGXZwBg==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:6muC3Zqh,iv:gBaC+6MXvZY/bIpy/cEG/zWwLBaUFMVfxAHa1OnwfnQ=,tag:v6NYyhAKAwovluHAXNaOVA==,type:str]
|
||||||
|
stringData:
|
||||||
|
SECRET_ENCRYPTION_KEY: ENC[AES256_GCM,data:jX9ULlaBYTSTFGRMplSder5rMrsz7THU3kgJSGef0r5rZvtoaAG9NVYU2QUEuD0sZgfqRooeMOkDCAqFc8qsuw==,iv:uOmx8w3XPKnoxfasf7gqi/bljxigpLwaAC30c2TroKg=,tag:Dmbu6m3+UZa1w0UR0LRh9g==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6TDVyNGRtYWgzMDYwdjdr
|
||||||
|
NXJWcGJZNUx6YzBGWm1nWlp0SlN3WXhaR0VBCjZ3bDBmUTFCUjg3U21wak90eHZK
|
||||||
|
NlRCTUZQSHhoS0ZSUGZ4UHhST01KNmMKLS0tIGUxcGo0NmJ2bmJ6eGJ3QWxzYVM5
|
||||||
|
c2Z6UkFjQmZ4ejIvK1JQQkZ0VFFIZ3cKA8UzmGsX+s3yOermOHqbnnEtekHCTqmC
|
||||||
|
R3Jyzf6OolZMJpNwbetcsOfgO61aM7LHl/Xb/hFLbLjZ+eECrt8VpA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:HTrEEVDm8JqnVBFaY/FJknxma0nI95tRiwVc0K76k+2R4jgKCBA3EUAzPVS+gA4rTAeKHPSHrT2eidlkseT2WQUjqMRrpHcUsbSjNfpTEY2g/U0PIiOXZMRGfwmsQVgmu2fqeMOyzDSyya0scdWJXPraRPNZ+OpEkNVlm4rWcgs=,iv:KY3dnnmooJgsu66wc7gbzeIGUXZNgVrxAhpAwxoG+G8=,tag:7r6d41oTD136N57rtY7auA==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:GjY=,iv:OKYWMFG0fqNwzg1F6Jjoon/DReOXEE6RPM5g0Dn6bF8=,tag:qLjLi9LCOuWRjagVXpMvaw==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:0gG77TDH,iv:9WJrkz0BDDllNZskrkqiQ2p1KYWU+EmKL+vetxIIGKE=,tag:UhU8fbO357HtxHAsVFeeBA==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:Lwk4A9vqN3cnrI8MrQepxf1w,iv:AZUeVqCpdrYJM/chE9bzmFOK5ceqPjqsqMDcQwliyr4=,tag:vbY81yhptOmD19UITJqFmw==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:UTS0X0QkMssEiH1l,iv:6ubXogCM03/cTdsUP+JWOSepjQc+ZamObyxINKspHeU=,tag:jOj2q0ZgKxWMxkrgfOct2Q==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:ADb2WnTAeWRVIHdzvP/Mumc=,iv:h1CCbwf3HqYWOyTX78GMfvnn+PGfrKWuxahe7/j9aCQ=,tag:bt6xtaqHPZ+E1wEGKgKDww==,type:str]
|
||||||
|
stringData:
|
||||||
|
tls.crt: ENC[AES256_GCM,data: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,iv:AdR04DL4zz35+2ajdPURF/Hya7YJh730XaPr0FIV9k4=,tag:SEnPSA1SrQIhoSM9mUDKdQ==,type:str]
|
||||||
|
tls.key: ENC[AES256_GCM,data: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,iv:tM8kJdNlKHgDmFQ85KAvt8nF9aPo9FeyQ8SoeYwQSd0=,tag:OKhz5KYHi2tCts+9MjY5KQ==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMZitSSzhtNitMM0h0RUgw
|
||||||
|
NmVGT2RhVm1jcUV6WGxPbUJmMytpOUR3c1hJCmlncC9PZkRMMGZrSWViL2dVZ2V4
|
||||||
|
emNYekZHbFJ3WWVUcjhZK1hPSTQ0UDgKLS0tIE1yeEIyZHBaVDA1MzNDMnBXcDdL
|
||||||
|
VjJTY0NoaFZhT0V5RU5hTlc1b1BPY1EKSiudyVo2Tw3PTPGt59vRL8pkJlw4zFbx
|
||||||
|
OTkdkmlcjwXPahnQacMQQOn0ndo/w4yHinu3RA+/yAB2yR1BPn8uQw==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T20:55:07Z"
|
||||||
|
mac: ENC[AES256_GCM,data:DS2iyn36jrJgpWdocrloEFyXcSEPc6vXVj8XJwvEDyBAkqulKDeVQy4Lm60pMWqAt83Q99WWvTq2xd6eRrtAWGO8xmeFrKWaldUqFdXs5l7uscSAm8gQU7Mxp0/bx7rhJECoa/MJp+kv1SeDuon1b838vaY1eekMn/nJTSFbcBE=,iv:5mEWwF7ov63rA1JvFpizLhZne30hEuTcFlHu1lyAsUg=,tag:R+ig+kp2MtZMF/rXabCyBQ==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
# All homelab SOPS-encrypted Secrets, decrypted in-line via the ksops generator.
|
||||||
|
# Each *.enc.yaml carries its own metadata.namespace, so no namespace transformer
|
||||||
|
# here (that would rewrite every Secret into one namespace). Renders exactly the
|
||||||
|
# Secret objects — replaces the old argocd-cmp-cm SOPS plugin.
|
||||||
|
generators:
|
||||||
|
- secret-generator.yaml
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:44k=,iv:X+aWGTBFUqzlKT6iCR/LN5ldJcT0mSt9q3bMYbm/094=,tag:WDq6/nb4lzmyJ54QmrkDEQ==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:Rzk5Z+Hy,iv:WGtDcuwwxveu72V1Ri+rh44IQL4CixKsISW6R6Ai1AA=,tag:fTqJaqQpA4yDHd8hY628rQ==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:XOUsgfr64/dKRSMt6g==,iv:78DvUfW9uxmVSr4LK5HS/GlTchRsaTBEMmqjtHwAwcg=,tag:EvmBXKJSX3iQyjojAX7T0w==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:xWSxbuMa1Q==,iv:FDj3unj17suAuxzow1iLUztImC0dT5e5QuzY6uPQXV4=,tag:kp9Qn4/uWPgIFfoEchsOgQ==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:luksVJs2,iv:gYL9JD63cgNp3mKqPc9liLdO9n/+qOK+Eb92uV5Ex7c=,tag:ECPwT8UUnLgr3OcMvhdg8A==,type:str]
|
||||||
|
stringData:
|
||||||
|
access_key_id: ENC[AES256_GCM,data:ZPX1IGcFZfxquA==,iv:9jKQl0nZYc5qfO9mdvfHhfG1a/0+mZ+tmZm20LQ2gg0=,tag:Rlq90E6Wq7el0MEiQDmIFA==,type:str]
|
||||||
|
secret_access_key: ENC[AES256_GCM,data:49BtPfJS57tifvyoO5DhK9os+O52b7igVSjdB1h86trEsXPle7KaUCkFXus=,iv:Uvy3pUpYlsZQkxMG2tKnXYD7Us4RcP1DmGWlNlUp0Ek=,tag:SrVny6/3Gt8XO4oI/VTmGQ==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2MFh4Y2xTelhGNXV4SG5x
|
||||||
|
TTROWS9LOXViRmRnM0FDajBYTUlnZ1R2M0RjCkp1Wk1LY1E0YXV4cWpvZDZ3S1Yz
|
||||||
|
SmNaaGxZVjdBQW55WDZqckh0aDkrNVkKLS0tIDVkZzdQeERybm0vZUQvZlhCK2hI
|
||||||
|
T2ZlWVJaMVBxanFIYStZRi82dGZxL1kKfQQoQlY3vVbU3Ys7TiEbDzCv5zFuki2m
|
||||||
|
LSJ3zaaRAm5ldnfvX6N7fiTJed7Qo/rbmMoDSCrxj5P0SA4UhCefEA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T20:43:24Z"
|
||||||
|
mac: ENC[AES256_GCM,data:qxnq1Rc2DYN8VI1V9Qbsg28dZALu1dozbkcWpGA5T8qAHoH8ORo16ZgS3RclAmKys0n05DzvNtnVh2jXWupg/OW8K4Kn7Mrcb6AmEi5XZAlkCy6tzZEKdm22h4A07wy12701nIf5Rn2dKt9PO60oDZeR/5S9oov47sDXjy578WY=,iv:fyqI/RVnvb2MsGrxNO0GU0+HYgLpAm3PXXOnDGkAaNQ=,tag:dcguaqaj+trqgBfiGdhZTw==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:Mdg=,iv:u/JjNJfdyz7ehk8lHZRh1u9zkNuufHosyXv10Gm+T6E=,tag:pkgZVJid6kpGuy61+oEMlQ==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:/kWhhqyz,iv:DymezGpBHmKT3BM/CcUKEGIWZw/JXNutReSKfhfmoD4=,tag:DnA+316Z+0eqBWN8YLyv1A==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:xs1k//nWy1iJ52Q=,iv:NDGvM319FCypxFLWwONcF12osaU/EL0IwD0c2xtqYnU=,tag:6IhfISF08WQbxrUmT49d7g==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:jpUzI7BmZA==,iv:PQrfm67la6RJCRq0H7qpiY8n2hPUjcaK6gmJ/zPQW30=,tag:bp/3GYbRpYOIub5nT0zz/A==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:fndXB4Y1,iv:EXOe0day1ScxvZ7ozn1QyHgaZgnvlPNmvSQG74aw7lM=,tag:quPmKvFTjRqtZoVUEoQAHQ==,type:str]
|
||||||
|
stringData:
|
||||||
|
config.env: ENC[AES256_GCM,data:3RIQ1i6NtY1Tv2eJY8JXSgfBEqi9px/9TJVNrPf9+brtGy9Mx3SwjKVnjGXUcA2cYcNZb1JR7temXmznXQdsRfZ9yohe4oc1vL3yPGPY9YfP7AHGYj9E7tghu/rgkHOf93RmfdKv0Rrq6miBcA==,iv:0RQVMi2mMHBRTJ1OX6IDFvq9Qr7zpqgQzt+MPPgBIHA=,tag:HNXLKf5X4P3E+hc7YbDtNQ==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtUVJFQ1ErNmkzd1N6b29t
|
||||||
|
L0VWR2tlcEIxeG1CR1hXRkw2cEt0Q2crMWlVClg2UlNhUzdJZGpLZjJtYlAvbk16
|
||||||
|
eHk0ZHZiNzVRTWhyQzlieTU5Y3QraGsKLS0tIHA5UVhvUXhNSDk0QlV0R2RzR1dP
|
||||||
|
azVEQzgvSFNwak9TcW1USFNOb3RvSkUKNLuW3h3cHDSSCZGe9Vyiv5m+wXihtCYX
|
||||||
|
oJtbRK+kJF8oOUo4emI+SLGrCsWq5cG2wN/wgP+ChR1zUgRgfq5oOA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:hQfimKuhfiIiZQAyNDZQR25HZ5biU+kaWig3ElVPiqMY3B0LrmKyOEs7ittd3j3meUkPEFSmm1yYSXcdCuf6w7cwSbEiL5DG8t7QKPbnx/aRdyHAJW+1VC0e3Lye43gaHya0iUaWS/YFQcrT01oEbckLIDWPKfvA6pg2bZ4kMZ4=,iv:AydyWMVcjTQu+7Fzw5vtqkvTTeQEMt8y6snESoLUDE8=,tag:SoKklNqa9nH9G6d9+PnolQ==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
|
---
|
||||||
|
apiVersion: ENC[AES256_GCM,data:Mdg=,iv:u/JjNJfdyz7ehk8lHZRh1u9zkNuufHosyXv10Gm+T6E=,tag:pkgZVJid6kpGuy61+oEMlQ==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:/kWhhqyz,iv:DymezGpBHmKT3BM/CcUKEGIWZw/JXNutReSKfhfmoD4=,tag:DnA+316Z+0eqBWN8YLyv1A==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:XEQHsQqTgwukYw==,iv:wTw2eoooDA9EfB2df3jzb0IWUdtLSwcuKzx8yDBaihQ=,tag:oncXt2gM1k6Qppafpu/D1w==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:jpUzI7BmZA==,iv:PQrfm67la6RJCRq0H7qpiY8n2hPUjcaK6gmJ/zPQW30=,tag:bp/3GYbRpYOIub5nT0zz/A==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:fndXB4Y1,iv:EXOe0day1ScxvZ7ozn1QyHgaZgnvlPNmvSQG74aw7lM=,tag:quPmKvFTjRqtZoVUEoQAHQ==,type:str]
|
||||||
|
stringData:
|
||||||
|
MINIO_IDENTITY_OPENID_CLIENT_SECRET: ENC[AES256_GCM,data:IYBfNW19cTo8WkCGT4zuCljzi2bkbYWJ8DY/yedR2MJNqEO+bMVvjx0OqSqbPp6rnwGyMQOhBN0gXPbEVRDBGg==,iv:I1aGIikbHTKvh//4Inhp7UoNfhjlIXSpcyP4RqljvqM=,tag:Zh3Vh5DQYAsTtiW59D0/QA==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtUVJFQ1ErNmkzd1N6b29t
|
||||||
|
L0VWR2tlcEIxeG1CR1hXRkw2cEt0Q2crMWlVClg2UlNhUzdJZGpLZjJtYlAvbk16
|
||||||
|
eHk0ZHZiNzVRTWhyQzlieTU5Y3QraGsKLS0tIHA5UVhvUXhNSDk0QlV0R2RzR1dP
|
||||||
|
azVEQzgvSFNwak9TcW1USFNOb3RvSkUKNLuW3h3cHDSSCZGe9Vyiv5m+wXihtCYX
|
||||||
|
oJtbRK+kJF8oOUo4emI+SLGrCsWq5cG2wN/wgP+ChR1zUgRgfq5oOA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:hQfimKuhfiIiZQAyNDZQR25HZ5biU+kaWig3ElVPiqMY3B0LrmKyOEs7ittd3j3meUkPEFSmm1yYSXcdCuf6w7cwSbEiL5DG8t7QKPbnx/aRdyHAJW+1VC0e3Lye43gaHya0iUaWS/YFQcrT01oEbckLIDWPKfvA6pg2bZ4kMZ4=,iv:AydyWMVcjTQu+7Fzw5vtqkvTTeQEMt8y6snESoLUDE8=,tag:SoKklNqa9nH9G6d9+PnolQ==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
|
---
|
||||||
|
apiVersion: ENC[AES256_GCM,data:Mdg=,iv:u/JjNJfdyz7ehk8lHZRh1u9zkNuufHosyXv10Gm+T6E=,tag:pkgZVJid6kpGuy61+oEMlQ==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:/kWhhqyz,iv:DymezGpBHmKT3BM/CcUKEGIWZw/JXNutReSKfhfmoD4=,tag:DnA+316Z+0eqBWN8YLyv1A==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:rYX7nN6SYoG5HycaNNrprDM=,iv:4uX7+Sl1UzgsImBH+qm9p8DlOfJu0i0W3bpoE7cFxXg=,tag:RROCnYBVKVwPBNSGgV30FQ==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:jpUzI7BmZA==,iv:PQrfm67la6RJCRq0H7qpiY8n2hPUjcaK6gmJ/zPQW30=,tag:bp/3GYbRpYOIub5nT0zz/A==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:fndXB4Y1,iv:EXOe0day1ScxvZ7ozn1QyHgaZgnvlPNmvSQG74aw7lM=,tag:quPmKvFTjRqtZoVUEoQAHQ==,type:str]
|
||||||
|
stringData:
|
||||||
|
CONSOLE_ACCESS_KEY: ENC[AES256_GCM,data:FhXl6yqTIzHsbJO6P96KQrA=,iv:gGTaSKxk721GwMQWWyymqUPZlGLidrBzWCWdRtnlOr0=,tag:IaVl8GQqPstBuUjD4N6JTQ==,type:str]
|
||||||
|
CONSOLE_SECRET_KEY: ENC[AES256_GCM,data:s22GOXY4B/m+S7tgDw8P2gU2F15vdn1tqAp53FWzFTI=,iv:GXBhHQ8YiaVjRRMw2yG4FxdulVOuOurkLXXC0u/SFRA=,tag:pvHcD9+4qaxnvhOSkNxoHA==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtUVJFQ1ErNmkzd1N6b29t
|
||||||
|
L0VWR2tlcEIxeG1CR1hXRkw2cEt0Q2crMWlVClg2UlNhUzdJZGpLZjJtYlAvbk16
|
||||||
|
eHk0ZHZiNzVRTWhyQzlieTU5Y3QraGsKLS0tIHA5UVhvUXhNSDk0QlV0R2RzR1dP
|
||||||
|
azVEQzgvSFNwak9TcW1USFNOb3RvSkUKNLuW3h3cHDSSCZGe9Vyiv5m+wXihtCYX
|
||||||
|
oJtbRK+kJF8oOUo4emI+SLGrCsWq5cG2wN/wgP+ChR1zUgRgfq5oOA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:hQfimKuhfiIiZQAyNDZQR25HZ5biU+kaWig3ElVPiqMY3B0LrmKyOEs7ittd3j3meUkPEFSmm1yYSXcdCuf6w7cwSbEiL5DG8t7QKPbnx/aRdyHAJW+1VC0e3Lye43gaHya0iUaWS/YFQcrT01oEbckLIDWPKfvA6pg2bZ4kMZ4=,iv:AydyWMVcjTQu+7Fzw5vtqkvTTeQEMt8y6snESoLUDE8=,tag:SoKklNqa9nH9G6d9+PnolQ==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: viaduct.ai/v1
|
||||||
|
kind: ksops
|
||||||
|
metadata:
|
||||||
|
name: sops-secret-generator
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/function: |
|
||||||
|
exec:
|
||||||
|
path: ksops
|
||||||
|
files:
|
||||||
|
- authentik-secrets.enc.yaml
|
||||||
|
- cloudflare-secrets.enc.yaml
|
||||||
|
- forgejo-runner-token.enc.yaml
|
||||||
|
- forgejo-secrets.enc.yaml
|
||||||
|
- grafana-oidc-secrets.enc.yaml
|
||||||
|
- grafana-secrets.enc.yaml
|
||||||
|
- homarr-auth-oidc.enc.yaml
|
||||||
|
- homarr-db-encryption.enc.yaml
|
||||||
|
- homarr-secrets.enc.yaml
|
||||||
|
- homelab-ca-secrets.enc.yaml
|
||||||
|
- loki-secrets.enc.yaml
|
||||||
|
- minio-secrets.enc.yaml
|
||||||
|
- vault-secrets.enc.yaml
|
||||||
|
- vault-unseal-keys.enc.yaml
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:cM0=,iv:KFms9ElUhHn+w9flx5nzLfwvfJsMN0u8+YDMqFgB6V8=,tag:7Ve53FzTefhTYFVwnkT/iA==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:khv7iEsd,iv:rzn3Pt2pJcbDj55NYafUamNLTBrtluUPeLKorzpskGA=,tag:0PbEGGlxwz8ixBfwdOPi0w==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:jgWuWAVI4nQMy8dkJXZI5L4=,iv:f9pwyirRf/mLqr8LhuNPE53guP5mlZmPftNhtFKSbFA=,tag:YWX+qDU1z82FGSw/Wc3nJA==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:j77p,iv:rkU3kw12xPeVkVRn3w8WIZlc0+11jGeg1jZBX3kRfaQ=,tag:c2MrHHI4N8CeEQYDAr5wZg==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:F4hK9q+O,iv:7gmqUtqcYzDHp99fYdinL4yYSTvjoyHlTnhHbEynGQY=,tag:Yuw+B+h81bbZbWcd1zMSsA==,type:str]
|
||||||
|
stringData:
|
||||||
|
access_key: ENC[AES256_GCM,data:SFP7ZsrqSJXbaA==,iv:QuGF69veC8FWOSAbwzv7vhKYkF0yOp0ty/U0hdmroAA=,tag:5VNvfVONHySMbYcYnwSajw==,type:str]
|
||||||
|
secret_key: ENC[AES256_GCM,data:WxvqVZJWTckEb3paILxUJ51hkg0aNzRJzziuopsCpY/Amita3zXl91VJtGE=,iv:dyKnNC8255FCmOd61xczuk0Ww9MYZmHCDc+JY60AjIQ=,tag:rzem3XDOj7VGaYuhk0dc2g==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkaFFHVDhicUVPWEZHaXJn
|
||||||
|
eGc3WmdUYk9mdHp4RFExazRhWHBDa3VLN3gwCmZCUFBJaHRaSmtPRGhsQkRHSFJt
|
||||||
|
U2NUMUh4azR3NllLWDg5ek94ZlFSYU0KLS0tIE1BOU1IMVAveWtKaEE3QWhFeXp6
|
||||||
|
ZGd0TVZxbldTLzhHK1YrQUNCWUJVZlkKzK/aF0+qohgujyKHiRpQ+YYsnfVkvzDr
|
||||||
|
+3VnWcU1dPH63wqtf10LeYnBq8ewesfkibOVlJx0v1z6nQ9ZTMXwqQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T18:02:01Z"
|
||||||
|
mac: ENC[AES256_GCM,data:7JKz0tx066VLgwnpgA2/aUK/A7KD5y2PZJUqRNPMfEmEFqpkvm3Z5Ejj/kxmqYHwsXL+P92SrvMnbqq0v5I6PolR3fPW5fT/xcLibBmvoh8HF4wrRoaElK4ojTNDYJHginy/NE34FwL6sgDCbtT8QAJl9k8HEwxHlfBeyzzryaw=,iv:oTuLhckDQlvLHhGYNrpU0CSQB4Bunv2XvSsD53Z+yZQ=,tag:7CEOFC5t7vbFUQvencL/sQ==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:Gn8=,iv:DmWiGvlY/rBzp1Yh6yKw9pXjFO7smSB1fKfe7agSJz8=,tag:X6KSIbxlZWYYXKKxOfuThw==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:lsjz9hz4,iv:vLq5BRMjNRl2ZGDQSrgff7NiZQsBEMSl6OTZ5dAMisk=,tag:ug62YjRZXbN1Y7PoMihsIw==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:TkkvNs86o2+hC8MAi8lEeU8=,iv:SaxeZgpWekHMJeoHKaoZYGPEVGw42XUk/sT6SaKGVN0=,tag:mtA9SwNKiXh3NrC5rdasRg==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:wK6m,iv:KtA31Bo8aGE1HU8H9KWbMwt2NfywWfy77G/LaReaI1E=,tag:V9SSlPawRQw0x2utFNN1aw==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:6UPZ1ZTR,iv:FxN1ebrlJ4IO3eDGEYSvktSpMgAebcl0DO1WHh5O0+0=,tag:2bZN4zdbHzx0oR+bTJPPJg==,type:str]
|
||||||
|
stringData:
|
||||||
|
key1: ENC[AES256_GCM,data:dV2HOh7W1Pl0QDJaGvtEKpBppybQMiK5kxzyk05zkA352ZmJvE8Ppm5yWKk=,iv:grQ8v2o/LHpJnIZonjtgTHKcLUQIH8xl1FtWtEs4rEs=,tag:lANcccYT6Ouo/0IcoLw4uA==,type:str]
|
||||||
|
key2: ENC[AES256_GCM,data:YSdpYL8h56PfUrvMFhBXhmBG2en0toLKEpYlJqwjAk/vI0jm7gq/TqGn034=,iv:GxthkNLhm3qHxjkZetiYl58Qa/K7G2ib6E+LWK16H8Q=,tag:q79UUoh6uCcMZJ0U18Ireg==,type:str]
|
||||||
|
key3: ENC[AES256_GCM,data:sO7Lao9qkmIOdARL+6FBcLo+e4z8LzMEzNrwcy5hv9uRuxppwAXrP0MO1vw=,iv:wSWe9h1HovRhV5YCZrmvcB2VEfRYQ+gasGO3uh8IpmQ=,tag:Bc5yaROQDTz1AhVnFDm/BA==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtekFMUWlnb2tiUVhCODE1
|
||||||
|
TkNCclVTa0dVUFhlc0l2elUrVi9MUytSM3pRCkdtYlRFL1cwTHNPZnFicHZwUnZU
|
||||||
|
eENhS0dIek1JbzZEODFSY2tyem91MUUKLS0tIFIzQzNmcERZM0hBbGI0TndEeGlB
|
||||||
|
MFk1RTBFbHRqempyYlQ2U05YVko2R1EKVvk1Rd9ZU0G1GHX+3mmlHAfQOPzmoPsk
|
||||||
|
7RltNZF2SCxjIv5C2pqf3CgmRBaQGWgMybRRH5gdB87PLBKkPL3+HQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-08-12T20:52:24Z"
|
||||||
|
mac: ENC[AES256_GCM,data:kLjoBWmJ2bZ13EbuzgppahHKmCY/xtCi1R7xCEUbCP4FQfiVaC4qAbVftE4lTNlXa2mqCKsGPYZ/A3HV/4+a+it/pg0a+rj7E7DszhieWbZufHMJs+w4/Le8l8wFFE5aNW0wdvGTJZ0n4HBOdAkE1qN9ReaiaQgr0rk5ggOPG9g=,iv:MiDVUgcoKrP/gj4keqomo6OB12dmz9VoesTiHQTnBFM=,tag:cLK67Pym4BuvXLF1RUA8Gw==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: github-repo-creds
|
||||||
|
namespace: argocd
|
||||||
|
labels:
|
||||||
|
argocd.argoproj.io/secret-type: repo-creds
|
||||||
|
stringData:
|
||||||
|
type: git
|
||||||
|
url: [email protected]:[email protected]:Riotpiaole/riotpiao.homelab.com.git
|
||||||
|
sshPrivateKey: |
|
||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||||
|
QyNTUxOQAAACAqZaKCvVj9z9JtQ8kyNpE42siEoEoUXTftc4zz+dAerQAAAKg5eEfbOXhH
|
||||||
|
2wAAAAtzc2gtZWQyNTUxOQAAACAqZaKCvVj9z9JtQ8kyNpE42siEoEoUXTftc4zz+dAerQ
|
||||||
|
AAAECTvujulFODUs/5miSpkwqqovKyuK3TSOSXFR8tNYOYKyplooK9WP3P0m1DyTI2kTja
|
||||||
|
yISgShRdN+1zjPP50B6tAAAAIXJvY2tsaWFuZ0BSb2NrZGVNYWNCb29rLVByby5sb2NhbA
|
||||||
|
ECAwQ=
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# Homelab Bootstrap — Single-Cluster, GitOps-Ready
|
||||||
|
|
||||||
|
**Run once manually, GitOps forever after.**
|
||||||
|
|
||||||
|
This bootstrap breaks the ArgoCD ↔ Forgejo circular dependency by:
|
||||||
|
1. Installing infrastructure in correct dependency order
|
||||||
|
2. Pointing ArgoCD at a GitHub mirror initially
|
||||||
|
3. Cutting over to Forgejo once healthy
|
||||||
|
4. Using Helm for reproducible installs
|
||||||
|
5. Ensuring ArgoCD adopts (not duplicates) bootstrap resources
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Talos cluster running (terraform applied)
|
||||||
|
- kubectl configured (`KUBECONFIG` points at cluster)
|
||||||
|
- Helm 3 installed
|
||||||
|
- SOPS age key at `~/.sops/homelab-age.key`
|
||||||
|
- GitHub mirror of this repo (for initial ArgoCD source)
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
bootstrap/
|
||||||
|
├── phase1-storage/ # Longhorn via Helm
|
||||||
|
├── phase2-cnpg/ # CNPG operator via Helm
|
||||||
|
├── phase3-forgejo/ # Forgejo DB + Forgejo via Helm
|
||||||
|
├── phase4-argocd/ # ArgoCD via Helm → GitHub initially
|
||||||
|
└── phase5-cutover/ # Switch ArgoCD source to Forgejo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From repo root:
|
||||||
|
./bootstrap.sh
|
||||||
|
|
||||||
|
# Or step-by-step:
|
||||||
|
./bootstrap.sh phase1 # Storage
|
||||||
|
./bootstrap.sh phase2 # CNPG
|
||||||
|
./bootstrap.sh phase3 # Forgejo
|
||||||
|
./bootstrap.sh phase4 # ArgoCD (GitHub mirror)
|
||||||
|
./bootstrap.sh phase5 # Cut over to Forgejo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
1. **DRY**: Helm values used by both bootstrap and ArgoCD
|
||||||
|
2. **Single Source of Truth**: Manifests match what ArgoCD will manage
|
||||||
|
3. **Idempotent**: Can re-run phases safely
|
||||||
|
4. **Adoption Ready**: Resources have `argocd.argoproj.io/sync-options: Prune=false`
|
||||||
|
5. **Dependency Ordered**: Each phase waits for previous to be Ready
|
||||||
|
|
||||||
|
## Phase Details
|
||||||
|
|
||||||
|
### Phase 1: Storage (Longhorn)
|
||||||
|
|
||||||
|
Installs Longhorn with:
|
||||||
|
- 3-node HA configuration
|
||||||
|
- Unified `longhorn` StorageClass (default)
|
||||||
|
- Special `longhorn-cnpg` StorageClass with postgres UID/GID mount options
|
||||||
|
- CSI plugin tolerations for control-plane nodes
|
||||||
|
|
||||||
|
**Source of Truth**: `phase1-storage/longhorn-values.yaml`
|
||||||
|
|
||||||
|
### Phase 2: CNPG Operator
|
||||||
|
|
||||||
|
Installs CloudNativePG operator with:
|
||||||
|
- CRD registration (blocks until CRD available)
|
||||||
|
- Webhook configuration
|
||||||
|
- Monitoring enabled
|
||||||
|
|
||||||
|
**Source of Truth**: `phase2-cnpg/cnpg-values.yaml`
|
||||||
|
|
||||||
|
### Phase 3: Forgejo Database + Forgejo
|
||||||
|
|
||||||
|
1. Creates `forgejo-db` CNPG Cluster
|
||||||
|
2. Waits for cluster Ready (PostgreSQL accepting connections)
|
||||||
|
3. Installs Forgejo via Helm pointing at `forgejo-db-rw` service
|
||||||
|
4. Waits for Forgejo healthy
|
||||||
|
|
||||||
|
**Source of Truth**:
|
||||||
|
- `phase3-forgejo/forgejo-db.yaml` (CNPG Cluster CR)
|
||||||
|
- `phase3-forgejo/forgejo-values.yaml` (Helm values)
|
||||||
|
|
||||||
|
### Phase 4: ArgoCD (GitHub Mirror)
|
||||||
|
|
||||||
|
Installs ArgoCD via Helm, then applies root app-of-apps pointing at **GitHub mirror**.
|
||||||
|
|
||||||
|
This is the circle-breaker: ArgoCD syncs from GitHub (not Forgejo) initially.
|
||||||
|
|
||||||
|
**Source of Truth**:
|
||||||
|
- `phase4-argocd/argocd-values.yaml`
|
||||||
|
- `phase4-argocd/root-app-github.yaml` (repoURL = GitHub)
|
||||||
|
|
||||||
|
ArgoCD **adopts** Phases 1-3 resources (no duplication) because manifests match.
|
||||||
|
|
||||||
|
### Phase 5: Cut Over to Forgejo
|
||||||
|
|
||||||
|
1. Push repo to Forgejo
|
||||||
|
2. Update root app `repoURL` from GitHub → Forgejo
|
||||||
|
3. ArgoCD re-syncs from Forgejo
|
||||||
|
|
||||||
|
**The circle is broken. GitHub mirror is now disaster recovery only.**
|
||||||
|
|
||||||
|
## Post-Bootstrap
|
||||||
|
|
||||||
|
All changes via Git:
|
||||||
|
```bash
|
||||||
|
git commit -m "feat(app): add new service"
|
||||||
|
git push forgejo main
|
||||||
|
# ArgoCD auto-syncs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Phase stuck?** Check `kubectl get events -n <namespace> --sort-by='.lastTimestamp'`
|
||||||
|
- **ArgoCD duplicating?** Verify manifests match exactly (Helm values ↔ ArgoCD Application)
|
||||||
|
- **Forgejo won't start?** Check CNPG cluster Ready: `kubectl get cluster forgejo-db -n cicd`
|
||||||
|
- **Can't push to Forgejo?** Verify ingress-nginx healthy, DNS resolves `forgejo.riotpiao.com`
|
||||||
|
|
||||||
|
## Migration from Old Bootstrap
|
||||||
|
|
||||||
|
If you have existing `k8s/bootstrap-local/`:
|
||||||
|
|
||||||
|
1. **DO NOT delete** existing resources (Longhorn data!)
|
||||||
|
2. Run refined bootstrap in "adoption mode" (no delete, just apply)
|
||||||
|
3. Verify ArgoCD shows "Synced" for all apps
|
||||||
|
4. Archive old bootstrap: `git mv k8s/bootstrap-local k8s/archive/bootstrap-local-v1`
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# k8s/cert-manager/cert-manager-values.yaml
|
||||||
|
# cert-manager issues every per-hostname cert off homelab-ca and renews the
|
||||||
|
# wildcard cert nginx serves — certmanager_certificate_expiration_timestamp_seconds
|
||||||
|
# is the early-warning signal before any service's TLS breaks.
|
||||||
|
|
||||||
|
# CRDs must be installed by the chart — ArgoCD helm-template does not apply a
|
||||||
|
# chart's crds/ folder, and the ClusterIssuer/Certificate resources (wave 1)
|
||||||
|
# depend on them existing first.
|
||||||
|
installCRDs: true
|
||||||
|
|
||||||
|
# Tolerate the control-plane taint (talos-cp-1 is the only schedulable node).
|
||||||
|
tolerations:
|
||||||
|
- key: node-role.kubernetes.io/control-plane
|
||||||
|
operator: Exists
|
||||||
|
effect: NoSchedule
|
||||||
|
webhook:
|
||||||
|
tolerations:
|
||||||
|
- key: node-role.kubernetes.io/control-plane
|
||||||
|
operator: Exists
|
||||||
|
effect: NoSchedule
|
||||||
|
cainjector:
|
||||||
|
tolerations:
|
||||||
|
- key: node-role.kubernetes.io/control-plane
|
||||||
|
operator: Exists
|
||||||
|
effect: NoSchedule
|
||||||
|
startupapicheck:
|
||||||
|
tolerations:
|
||||||
|
- key: node-role.kubernetes.io/control-plane
|
||||||
|
operator: Exists
|
||||||
|
effect: NoSchedule
|
||||||
|
|
||||||
|
prometheus:
|
||||||
|
enabled: true
|
||||||
|
servicemonitor:
|
||||||
|
enabled: true
|
||||||
|
interval: 60s
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
# Public root cert for the self-signed homelab-ca, distributed so internal
|
||||||
|
# services (Authentik, forgejo-runner, management-service, blackbox-exporter)
|
||||||
|
# can trust certs cert-manager issues off the homelab-ca ClusterIssuer.
|
||||||
|
# Not sensitive — public cert only, no private key. One copy per consuming
|
||||||
|
# namespace since ConfigMaps aren't cluster-scoped.
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: homelab-ca
|
||||||
|
namespace: iam
|
||||||
|
data:
|
||||||
|
ca.crt: |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
homelab-ca.crt: |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: homelab-ca
|
||||||
|
namespace: cicd
|
||||||
|
data:
|
||||||
|
ca.crt: |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
homelab-ca.crt: |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: homelab-ca
|
||||||
|
namespace: monitoring
|
||||||
|
data:
|
||||||
|
ca.crt: |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
homelab-ca.crt: |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFBjCCAu6gAwIBAgIJAKoBPdKPDL5AMA0GCSqGSIb3DQEBCwUAMDAxEzARBgNV
|
||||||
|
BAMMCmhvbWVsYWItY2ExGTAXBgNVBAoMEHJpb3RwaWFvLmhvbWVsYWIwHhcNMjYw
|
||||||
|
ODEyMjA1NDQzWhcNMzYwODA5MjA1NDQzWjAwMRMwEQYDVQQDDApob21lbGFiLWNh
|
||||||
|
MRkwFwYDVQQKDBByaW90cGlhby5ob21lbGFiMIICIjANBgkqhkiG9w0BAQEFAAOC
|
||||||
|
Ag8AMIICCgKCAgEAzw796vZRFOBc/orUu9J6rv91A3SNLJsqz/VuetwAXCaVSlK5
|
||||||
|
kL/RaBKKkcXToCC5avwix6W21FnwLUW4R8QlQdin8+XEHpx2LY5R4GmSvaCbU7wm
|
||||||
|
YwYgVGQVv8KlgcMbz/pF09vXU29cHUHwlkN+BWVGAxVGWxam94j1pBzV0dOhhRw2
|
||||||
|
q5sZYUUrPO5l775od2FMMnMzoRFugxDvJof0p3fAChNAIiFhP95lrcb2YsTwK+a2
|
||||||
|
6mk3AZc0gMtmgVsH+zryGFcAmWLixq41ye/H23ktj2bjEHCr9mIDpuSOKLyLBzrg
|
||||||
|
js/HqdxvwkZkkxwXft0Vi7GN0/LGHMdPrDE499pJelAHbxUpBoOk8sfXSNnfESI8
|
||||||
|
v20LybATGrKXFlOhMKshY1a3JLAoZTaW0xkG4PM4HT7lK/ovr/NLF6kdP7HxJFFQ
|
||||||
|
+4WjySiwbjusNobpTaQWVwCjD3Imf+2feAqJeX3z+P3EhxfrmvEojLOdUiiPcjMn
|
||||||
|
CFl9u2EpSlax53umSkcBQJGfFKM17fZzhtakjqhCTzAfXXlxZy4hsHDtfzRcfaoB
|
||||||
|
Zz/PiJ0TtJSC7TvxZZoPpGexQdoeT7kJEksTvGi5OSfJD7HaKe5RQSZGgpz7ATB+
|
||||||
|
YMBKbjhMXVu+TAdcO1qZRN0UCncrliN61ki9F0tE8kNgpS9biZ6kheoJMZUCAwEA
|
||||||
|
AaMjMCEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcN
|
||||||
|
AQELBQADggIBAFN1At11d/+Ue6cEjCklnRQpLuD5agXaE+pi5oHNfnggSlO8UjUj
|
||||||
|
lDzfnmxi3IAI6HruzDARM9J95qJboUxWOAEmK9SNnZNS/1b0GyikP3YcF1gIFqtW
|
||||||
|
6SgmVfC1cVUFKGt/+QQzZxhOLuoydv9Ozu12nxsTW/WY2t+cpeL+1wrsM22RA6W/
|
||||||
|
xV/bzYbwzCUT9v4tMZLZHk5CkXztVFXSJyW/8w9rVZdUGZM+mwwn81PLSE0v3s/2
|
||||||
|
wJH8eUqGl73i3Xgn7CVanN4cJYMGYVVlRD6Qr6muBN4Vh0ETY1PrjzsZ6XsV6vk9
|
||||||
|
glMbQnErYMZHZIDtnVdaBt60MecUD9O4eLeip0SE6bVRAucKBXA9xNMilMQEL8/X
|
||||||
|
FBMAxXl10rowBlbX2oaFSOM80eLTla4a3gSL7+0vua3C4Dtv2bLo8ZjCyqXwOJeb
|
||||||
|
N85cqcVZStz3qlgQN1nG2qGE3BS6scOs7+mUGe9CmS8acJV++PKm8CtmLH6/ibjV
|
||||||
|
tAc/6+Vg/keo0kty7H7L6oQd+gKpuaJ2I+wKYe4l9UwHi9zEb8YjzGB4n2AtsoWI
|
||||||
|
NDKi7/Gxha790QN3TD/U7p9GcEtfXKv8KISKfxnfD9o9Yx6pWOXcgvGefQkpXuRm
|
||||||
|
kGzDM+aY/VKs5Et5/RyKavBDihuOcumLsmoRg/bFoB/xpeURTyiehUdu
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: homelab-ca
|
||||||
|
namespace: sqs
|
||||||
|
data:
|
||||||
|
ca.crt: |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
homelab-ca.crt: |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# k8s/bootstrap/cert-manager/homelab-ca-issuer.yaml
|
||||||
|
# Self-signed internal CA, for services that only need to trust each other
|
||||||
|
# on the homelab LAN (not exposed to the internet, no public ACME needed).
|
||||||
|
# Root cert+key live in homelab-ca-secrets.enc.yaml (cert-manager namespace).
|
||||||
|
# The public cert is separately distributed via homelab-ca-configmap.yaml so
|
||||||
|
# non-cert-manager pods (Authentik, forgejo-runner, etc.) can trust it too.
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: homelab-ca
|
||||||
|
spec:
|
||||||
|
ca:
|
||||||
|
secretName: homelab-ca-keypair
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
# Issuers + CA trust-bundle ConfigMaps for the cert-manager-issuers Application.
|
||||||
|
# Each resource sets its own metadata.namespace (the ConfigMaps target iam/cicd/
|
||||||
|
# monitoring/sqs; ClusterIssuers are cluster-scoped) — so NO namespace transformer
|
||||||
|
# here (that would rewrite them all into one namespace). cert-manager itself is a
|
||||||
|
# separate Helm Application; cert-manager-values.yaml here is only its $values ref.
|
||||||
|
resources:
|
||||||
|
- letsencrypt-issuer.yaml
|
||||||
|
- homelab-ca-issuer.yaml
|
||||||
|
- homelab-ca-configmap.yaml
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# k8s/bootstrap/cert-manager/letsencrypt-issuer.yaml
|
||||||
|
# Let's Encrypt ClusterIssuers for *.riotpiao.com, DNS-01 via Cloudflare
|
||||||
|
# (wildcard certs require DNS-01 — HTTP-01 can't prove ownership of a wildcard name).
|
||||||
|
#
|
||||||
|
# Prereq (apply manually, not in git — token is a secret):
|
||||||
|
# kubectl create secret generic cloudflare-api-token-secret \
|
||||||
|
# --namespace cert-manager \
|
||||||
|
# --from-literal=api-token=<CLOUDFLARE_API_TOKEN>
|
||||||
|
# Token scope: Zone:DNS:Edit for the riotpiao.com zone.
|
||||||
|
#
|
||||||
|
# Start with letsencrypt-staging to validate the solver, then point
|
||||||
|
# Certificate.issuerRef at letsencrypt-prod once staging issues cleanly.
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: letsencrypt-staging
|
||||||
|
spec:
|
||||||
|
acme:
|
||||||
|
server: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||||
|
email: [email protected]
|
||||||
|
privateKeySecretRef:
|
||||||
|
name: letsencrypt-staging-account-key
|
||||||
|
solvers:
|
||||||
|
- dns01:
|
||||||
|
cloudflare:
|
||||||
|
apiTokenSecretRef:
|
||||||
|
name: cloudflare-api-token-secret
|
||||||
|
key: api-token
|
||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: letsencrypt-prod
|
||||||
|
spec:
|
||||||
|
acme:
|
||||||
|
server: https://acme-v02.api.letsencrypt.org/directory
|
||||||
|
email: [email protected]
|
||||||
|
privateKeySecretRef:
|
||||||
|
name: letsencrypt-prod-account-key
|
||||||
|
solvers:
|
||||||
|
- dns01:
|
||||||
|
cloudflare:
|
||||||
|
apiTokenSecretRef:
|
||||||
|
name: cloudflare-api-token-secret
|
||||||
|
key: api-token
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: kube-system
|
||||||
|
resources: []
|
||||||
|
# Cilium deployed via Helm chart
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# k8s/cilium/l2-announcement-policy.yaml
|
||||||
|
# CiliumL2AnnouncementPolicy — without this, LB-IPAM (lb-ipam-pool.yaml)
|
||||||
|
# assigns IPs to LoadBalancer Services but nothing ARPs for them on the LAN,
|
||||||
|
# so they're unreachable from outside the cluster even though `kubectl get
|
||||||
|
# svc` shows a real EXTERNAL-IP. Confirmed both forgejo's .165 and
|
||||||
|
# shadowsocks' .166 were 100% packet loss / incomplete ARP before this.
|
||||||
|
#
|
||||||
|
# loadBalancerIPs: true makes Cilium announce every Service's LB-IPAM IP via
|
||||||
|
# ARP from whichever node currently holds the lease for it (one node per IP,
|
||||||
|
# decided by leaderElection — not all nodes simultaneously, which would
|
||||||
|
# otherwise cause ARP flapping/duplicate-IP confusion on the LAN).
|
||||||
|
#
|
||||||
|
# externalIPs/loadBalancerIPs split exists because Cilium also supports
|
||||||
|
# announcing Service externalIPs (a different field, unused in this repo);
|
||||||
|
# we only need loadBalancerIPs since every exposed Service here is type
|
||||||
|
# LoadBalancer via lb-ipam-pool.yaml.
|
||||||
|
#
|
||||||
|
# requires kube-proxy replacement (already the case — see
|
||||||
|
# k8s/talos-iam or helmfile.yaml.gotmpl kubeProxyReplacement=true) and a
|
||||||
|
# Cilium build with L2 announcements enabled (default since v1.14).
|
||||||
|
#
|
||||||
|
# Apply once after cluster bootstrap, alongside lb-ipam-pool.yaml:
|
||||||
|
# kubectl apply -f k8s/cilium/l2-announcement-policy.yaml
|
||||||
|
#
|
||||||
|
# Verify:
|
||||||
|
# kubectl get ciliuml2announcementpolicy
|
||||||
|
# ping 192.168.1.165 && ping 192.168.1.166 # both should now respond
|
||||||
|
# arp -a | grep 192.168.1.16 # should resolve to a real MAC
|
||||||
|
apiVersion: cilium.io/v2alpha1
|
||||||
|
kind: CiliumL2AnnouncementPolicy
|
||||||
|
metadata:
|
||||||
|
name: homelab-l2-announce
|
||||||
|
spec:
|
||||||
|
loadBalancerIPs: true
|
||||||
|
interfaces:
|
||||||
|
- eno1
|
||||||
|
# No nodeSelector restriction — all 3 nodes already run workloads
|
||||||
|
# (allowSchedulingOnControlPlanes: true in controlplane.yaml), and with
|
||||||
|
# 3 zone-labeled nodes, redundancy for per-IP leader election is maintained.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# k8s/cilium/lb-ipam-pool.yaml
|
||||||
|
# CiliumLoadBalancerIPPool — tells Cilium LB-IPAM which IPs it can assign
|
||||||
|
# to LoadBalancer services in this cluster.
|
||||||
|
#
|
||||||
|
# CIDR 192.168.1.160/28 covers .160–.175 on the LAN:
|
||||||
|
# .160 talos-cp-1 (node — not assignable to services)
|
||||||
|
# .161 reserved
|
||||||
|
# .162 talos-worker-1 (node — not assignable to services)
|
||||||
|
# .163–.175 free for LoadBalancer services
|
||||||
|
#
|
||||||
|
# Current service IP assignments (via io.cilium/lb-ipam-ips annotation):
|
||||||
|
# 192.168.1.165 forgejo-gitea-http (cicd)
|
||||||
|
# 192.168.1.165 forgejo-gitea-ssh (cicd) — same IP, different ports
|
||||||
|
# 192.168.1.166 shadowsocks (vpn)
|
||||||
|
#
|
||||||
|
# Apply once after cluster bootstrap:
|
||||||
|
# kubectl apply -f k8s/cilium/lb-ipam-pool.yaml
|
||||||
|
#
|
||||||
|
# Verify assignment:
|
||||||
|
# kubectl get svc -n cicd forgejo-gitea-http forgejo-gitea-ssh
|
||||||
|
# # EXTERNAL-IP should change from <pending> to 192.168.1.165
|
||||||
|
|
||||||
|
apiVersion: "cilium.io/v2alpha1"
|
||||||
|
kind: CiliumLoadBalancerIPPool
|
||||||
|
metadata:
|
||||||
|
name: homelab-pool
|
||||||
|
spec:
|
||||||
|
blocks:
|
||||||
|
- cidr: "192.168.1.160/28"
|
||||||
|
# DO NOT add any 10.6.0.0/24 block here. That is the WireGuard subnet
|
||||||
|
# (10.6.0.1 = talos-cp-1 tunnel IP, 10.6.0.2 = DNS — see
|
||||||
|
# cluster-config/controlplane.yaml). A 10.6.0.x block let Cilium LB-IPAM
|
||||||
|
# auto-assign the CP's own tunnel IP to a Service, which broke the
|
||||||
|
# WireGuard tunnel and locked out the default kubectl context. It also
|
||||||
|
# can't work over WireGuard anyway — L2 announcements only ARP on eno1
|
||||||
|
# (the LAN interface), not wg0. Keep this pool LAN-only.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Service alias for CoreDNS compatibility
|
||||||
|
# CoreDNS rewrites *.riotpiao.com → ingress-nginx-controller.ingress-nginx.svc
|
||||||
|
# But bootstrap deployed as ingress-nginx-bootstrap-controller
|
||||||
|
# This alias makes both names work
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: ingress-nginx-controller
|
||||||
|
namespace: ingress-nginx
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: ingress-nginx
|
||||||
|
app.kubernetes.io/instance: ingress-nginx-bootstrap
|
||||||
|
app.kubernetes.io/component: controller
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: http
|
||||||
|
- name: https
|
||||||
|
port: 443
|
||||||
|
targetPort: https
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
# k8s/ingress/ingress.yaml
|
||||||
|
# Ingress rules for all homelab services.
|
||||||
|
# TLS is handled centrally: nginx serves the wildcard-tls cert (*.riotpiao.com)
|
||||||
|
# as its default-ssl-certificate. No per-rule tls: blocks or cert-manager annotations
|
||||||
|
# are needed — cert-manager manages one cert, nginx uses it for all hosts.
|
||||||
|
#
|
||||||
|
# DNS: *.riotpiao.com must resolve to 10.6.0.1 (WireGuard) or 192.168.1.160 (LAN).
|
||||||
|
|
||||||
|
# ── Grafana ───────────────────────────────────────────────────────────────────
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: grafana
|
||||||
|
namespace: logging
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: grafana.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: grafana
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
|
||||||
|
---
|
||||||
|
# ── Loki (API access for external tools) ─────────────────────────────────────
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: loki
|
||||||
|
namespace: logging
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: loki.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: loki
|
||||||
|
port:
|
||||||
|
number: 3100
|
||||||
|
|
||||||
|
---
|
||||||
|
# ── Authentik ─────────────────────────────────────────────────────────────────
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: authentik
|
||||||
|
namespace: iam
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: authentik.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: authentik-server
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
|
||||||
|
---
|
||||||
|
# ── Vault ─────────────────────────────────────────────────────────────────────
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: vault
|
||||||
|
namespace: iam
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: vault.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: vault
|
||||||
|
port:
|
||||||
|
number: 8200
|
||||||
|
|
||||||
|
---
|
||||||
|
# ── MinIO console (storage namespace) ────────────────────────────────────────
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: minio
|
||||||
|
namespace: storage
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
|
||||||
|
nginx.ingress.kubernetes.io/affinity: "cookie"
|
||||||
|
nginx.ingress.kubernetes.io/session-cookie-name: "minio-console-affinity"
|
||||||
|
nginx.ingress.kubernetes.io/session-cookie-max-age: "3600"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: minio.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: minio-cluster-console
|
||||||
|
port:
|
||||||
|
number: 9090
|
||||||
|
|
||||||
|
---
|
||||||
|
# ── MinIO S3 API ──────────────────────────────────────────────────────────────
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: minio-api
|
||||||
|
namespace: storage
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: minio-api.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: minio
|
||||||
|
port:
|
||||||
|
number: 9000
|
||||||
|
|
||||||
|
---
|
||||||
|
# ── Prometheus ────────────────────────────────────────────────────────────────
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: prometheus
|
||||||
|
namespace: monitoring
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: prometheus.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: prometheus-kube-prometheus-prometheus
|
||||||
|
port:
|
||||||
|
number: 9090
|
||||||
|
|
||||||
|
---
|
||||||
|
# ── Portainer ─────────────────────────────────────────────────────────────────
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: portainer
|
||||||
|
namespace: dashboard
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: portainer.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: portainer
|
||||||
|
port:
|
||||||
|
number: 9000
|
||||||
|
|
||||||
|
---
|
||||||
|
# ── Forgejo (git forge + OCI registry UI) ────────────────────────────────────
|
||||||
|
# nginx terminates TLS using the wildcard cert, then proxies plain HTTP to
|
||||||
|
# Forgejo on port 3000. ROOT_URL stays https:// so Forgejo generates correct URLs.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: forgejo
|
||||||
|
namespace: cicd
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- forgejo.riotpiao.com
|
||||||
|
secretName: riotpiao-com-tls
|
||||||
|
rules:
|
||||||
|
- host: forgejo.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: forgejo-gitea-http
|
||||||
|
port:
|
||||||
|
number: 3000
|
||||||
|
|
||||||
|
---
|
||||||
|
# ── Argo CD (cicd namespace) ──────────────────────────────────────────────────
|
||||||
|
# argocd-server runs HTTPS internally — nginx proxies via backend-protocol: HTTPS.
|
||||||
|
# proxy-ssl-verify: off because argocd-server's pod cert is self-signed (not homelab-ca).
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: argocd
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-ssl-verify: "off"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: argocd.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: argocd-server
|
||||||
|
port:
|
||||||
|
number: 443
|
||||||
|
|
||||||
|
---
|
||||||
|
# ── Longhorn UI ───────────────────────────────────────────────────────────────
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: longhorn
|
||||||
|
namespace: longhorn-system
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: longhorn.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: longhorn-frontend
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
|
||||||
|
---
|
||||||
|
# ── Temporal Web UI ────────────────────────────────────────────────────────────
|
||||||
|
# Temporal workflow orchestration Web UI
|
||||||
|
# TLS: wildcard cert managed by cert-manager, served by nginx default-ssl-certificate
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: temporal
|
||||||
|
namespace: temporal
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: temporal.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: temporal-web
|
||||||
|
port:
|
||||||
|
number: 8080
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: homarr
|
||||||
|
namespace: dashboard
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: homarr.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: homarr
|
||||||
|
port:
|
||||||
|
number: 7575
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
# No top-level namespace - resources declare their own namespaces
|
||||||
|
resources:
|
||||||
|
- ingress-nginx-controller-alias.yaml # Service alias for CoreDNS compatibility
|
||||||
|
- riotpiao-com-cert.yaml # Certificate for *.riotpiao.com (ingress-nginx namespace)
|
||||||
|
- ingress.yaml # Ingress rules for all services (multiple namespaces)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: ingress-nginx
|
||||||
|
labels:
|
||||||
|
pod-security.kubernetes.io/enforce: privileged
|
||||||
|
pod-security.kubernetes.io/audit: privileged
|
||||||
|
pod-security.kubernetes.io/warn: privileged
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user