refactor(k8s): consolidate to infra/+apps/ single-source tree, dedicated per-app CNPG (authentik-db/temporal-db), wire monitoring-config, forgejo→cicd ns, drop orphan/stale (data-schemas, ollama, story-crater, sqs/argocd, key-rotation)

This commit is contained in:
Story Crater Bot
2026-07-23 20:54:02 -07:00
parent 1c7395d9e1
commit e6f2ab1423
241 changed files with 467 additions and 10416 deletions
+2 -1
View File
@@ -52,4 +52,5 @@ skills-lock.json
secrets-plaintext.yaml secrets-plaintext.yaml
skills-lock.json skills-lock.json
CLAUDE.md .DS_Store
CLAUDE.md
-301
View File
@@ -1,301 +0,0 @@
# ArgoCD Application Consolidation Plan
## Executive Summary
**Current:** 39 Applications
**Proposed:** 32 Applications (Conservative: 35)
**Benefits:** Easier management, fewer Application resources, cleaner structure
## Phase 1: Safe & Easy Merges (Conservative - Start Here)
These are Applications that already share the same directory and have clear parent-child relationships.
### 1.1 Merge: wildcard-cert + homelab-ingress → "ingress-config"
**Current State:**
```
wildcard-cert (wave 1) → k8s/bootstrap/ingress/riotpiao-com-cert.yaml
homelab-ingress (wave 2) → k8s/bootstrap/ingress/ingress.yaml
```
**New State:**
```
ingress-config (wave 1) → k8s/bootstrap/ingress/ (kustomization)
```
**Rationale:** Both in same directory, ingress depends on cert existing
**Implementation:**
- Update `k8s/bootstrap/ingress/kustomization.yaml` to include both
- Create single Application pointing to k8s/bootstrap/ingress/
- Delete wildcard-cert and homelab-ingress Applications
- Result: 2 → 1 Applications
### 1.2 Merge: homarr + homarr-patches → "homarr"
**Current State:**
```
homarr (wave 8) → Helm chart (homarr-labs.github.io)
homarr-patches (wave 9) → k8s/applications/homarr/fix-probes-job.yaml
```
**New State:**
```
homarr (multi-source):
- Source 1: Helm chart
- Source 2: k8s/applications/homarr/ (patches as PostSync hook)
```
**Rationale:** homarr-patches is literally a patch for homarr
**Implementation:**
- Modify homarr Application to use multi-source
- Convert fix-probes-job.yaml to PostSync hook
- Delete homarr-patches Application
- Result: 2 → 1 Applications
### 1.3 Merge: temporal-db-secret-sync → temporal (as PreSync hook)
**Current State:**
```
temporal-db-secret-sync (wave 7) → k8s/applications/temporal/db-secret-sync/
temporal (wave 8) → Helm chart
```
**New State:**
```
temporal (multi-source):
- Source 1: k8s/applications/temporal/db-secret-sync/ (PreSync hook)
- Source 2: Helm chart
- Source 3: k8s/applications/temporal/ (values)
```
**Rationale:** db-secret-sync is a bootstrap job for temporal
**Implementation:**
- Modify temporal Application to use multi-source
- Ensure db-secret-sync Job has PreSync hook annotation
- Delete temporal-db-secret-sync Application
- Result: 2 → 1 Applications
### 1.4 Merge: cert-manager-issuers → cert-manager (as multi-source)
**Current State:**
```
cert-manager (wave 0) → Helm chart
cert-manager-issuers (wave 1) → k8s/bootstrap/cert-manager/ (issuers)
```
**New State:**
```
cert-manager (multi-source):
- Source 1: Helm chart
- Source 2: k8s/bootstrap/cert-manager/ (values + issuers)
```
**Rationale:** Issuers are part of cert-manager configuration
**Implementation:**
- Modify cert-manager Application to use multi-source
- Keep as wave 0 (issuers can wait for CRDs to be ready)
- Delete cert-manager-issuers Application
- Result: 2 → 1 Applications
**Phase 1 Result: 39 → 35 Applications (-4)**
---
## Phase 2: Logical Consolidations (Moderate Risk)
### 2.1 Merge: SQS Applications → "sqs-platform"
**Current State:**
```
strimzi-operator (wave 5) → Helm chart (operator)
kmsvc-redis (wave 5) → Helm chart (Redis)
kafka-cluster (wave 6) → k8s/applications/sqs/charts/kafka-cluster/
queue-crd (wave 6) → k8s/applications/sqs/charts/queue-crd/
management-service (wave 7) → k8s/applications/sqs/charts/management-service/
```
**New State:**
```
strimzi-operator (wave 5) → Keep separate (operator)
sqs-platform (wave 6) → k8s/applications/sqs/ (all services + CRDs)
```
**Rationale:** These 4 apps work together as the SQS platform
**Implementation:**
- Create kustomization.yaml in k8s/applications/sqs/
- Create single multi-source Application
- Keep strimzi-operator separate (it's infrastructure)
- Result: 5 → 2 Applications
### 2.2 Merge: authentik + iam-jobs → "authentik"
**Current State:**
```
authentik (wave 3) → Helm chart
iam-jobs (wave 3) → k8s/security/iam/ (authentik helper jobs)
```
**New State:**
```
authentik (multi-source):
- Source 1: Helm chart
- Source 2: k8s/security/iam/ (helper jobs)
```
**Rationale:** iam-jobs appears to be authentik-specific
**Implementation:**
- Verify iam-jobs are authentik-specific
- Merge into single multi-source Application
- Result: 2 → 1 Applications
### 2.3 Remove Duplicate: ingress-nginx Application
**Current State:**
```
ingress-nginx-bootstrap (wave 0, bootstrap) → Helm chart
ingress-nginx (wave 0, ArgoCD) → Helm chart (DUPLICATE!)
```
**New State:**
```
ingress-nginx-bootstrap (wave 0, bootstrap) → Keep only this one
```
**Rationale:** These are the same thing, bootstrap one is already working
**Implementation:**
- Delete ingress-nginx Application (redundant)
- Result: 2 → 1 Applications
**Phase 2 Result: 35 → 31 Applications (-4)**
---
## Phase 3: Advanced Consolidations (Consider Carefully)
### 3.1 Consider: Monitoring Stack
**Current State:**
```
prometheus-crds (wave 0) → k8s/platform/monitoring/crds/
prometheus (wave 1) → Helm chart
blackbox-exporter (wave 1) → Helm chart
```
**Option A:** Keep separate (CRDs must be wave 0)
**Option B:** Create "monitoring-stack" with prometheus + blackbox-exporter only
**Recommendation:** Keep separate for now (CRDs are special)
### 3.2 Consider: Logging Stack
**Current State:**
```
loki (wave 2) → Helm chart
promtail (wave 2) → Helm chart
grafana (wave 2) → Helm chart
```
**Option A:** Create "logging-stack" with all three
**Option B:** Keep separate (grafana used by multiple systems)
**Recommendation:** Keep separate for now (grafana has multiple consumers)
---
## Implementation Order
### Sprint 1: Phase 1 (Low Risk)
1. ✅ ingress-config (wildcard-cert + homelab-ingress)
2. ✅ homarr (merge homarr-patches)
3. ✅ temporal (merge db-secret-sync)
4. ✅ cert-manager (merge issuers)
### Sprint 2: Phase 2 (Moderate Risk)
5. ✅ Remove duplicate ingress-nginx
6. ✅ sqs-platform consolidation
7. ✅ authentik + iam-jobs
### Sprint 3: Phase 3 (Optional)
8. ⚠️ Evaluate monitoring stack consolidation
9. ⚠️ Evaluate logging stack consolidation
---
## File Changes Required
### For each consolidation:
1. **Update Application YAML** in `k8s/argocd/apps/*.yaml`
- Convert single-source to multi-source
- Update sync waves appropriately
- Add hook annotations where needed
2. **Update kustomization.yaml** in target directories
- Ensure all resources are listed
- Verify namespace declarations
3. **Test locally:**
```bash
kubectl kustomize k8s/bootstrap/ingress/
helm template <chart> -f values.yaml
```
4. **Delete old Applications:**
```bash
kubectl delete application <old-app> -n argocd
```
---
## Rollback Plan
Each consolidation is independent. If issues arise:
1. **Revert git commit** with the Application change
2. **Restore old Applications** from git history
3. **Force ArgoCD refresh:**
```bash
kubectl delete application <new-merged-app> -n argocd
kubectl apply -f k8s/argocd/apps/<old-wave-file>.yaml
```
---
## Success Metrics
**Before:** 39 Applications
**After (Conservative):** 35 Applications
**After (Full):** 31-32 Applications
**Benefits:**
- ✅ Easier to understand (related apps grouped)
- ✅ Fewer Application CRs to manage
- ✅ Cleaner ArgoCD UI
- ✅ Logical grouping by function
- ✅ Reduced sync complexity
**Risks:**
- ⚠️ Multi-source Applications are slightly more complex
- ⚠️ Sync wave ordering must be preserved
- ⚠️ Hook timing must be correct (PreSync/PostSync)
---
## Decision: Which Phase?
**Recommendation:** Start with **Phase 1 (Conservative)** → 35 Applications
This gives us:
- 4 fewer Applications to manage
- Low risk (same directories)
- Easy to understand merges
- Can evaluate Phase 2 later based on results
**Do you want to proceed with Phase 1 implementation?**
-188
View File
@@ -1,188 +0,0 @@
# ArgoCD Application Consolidation - Implementation Results
## Summary
**Before:** 39 Applications
**After:** 35 Applications
**Removed:** 4 Applications (-10.3%)
## Implemented Merges
### ✅ 1. ingress-config ← wildcard-cert + homelab-ingress
**Before:**
- `wildcard-cert` (wave 1): k8s/bootstrap/ingress/riotpiao-com-cert.yaml
- `homelab-ingress` (wave 2): k8s/bootstrap/ingress/ingress.yaml
**After:**
- `ingress-config` (wave 1): k8s/bootstrap/ingress/ (kustomization)
**Changes:**
- Updated `k8s/bootstrap/ingress/kustomization.yaml` to include both files
- Merged both Applications into single `ingress-config` in `00-substrate.yaml`
- Certificate created before Ingresses (wave 1)
---
### ✅ 2. homarr ← homarr + homarr-patches
**Before:**
- `homarr` (wave 8): Helm chart + values
- `homarr-patches` (wave 9): k8s/applications/homarr/ (PostSync hook)
**After:**
- `homarr` (wave 8): Multi-source (Helm + values + PostSync patches)
**Changes:**
- Added third source to homarr Application
- fix-probes-job.yaml already has PostSync hook annotation
- Removed homarr-patches Application from `60-applications.yaml`
---
### ✅ 3. temporal ← temporal + temporal-db-secret-sync
**Before:**
- `temporal-db-secret-sync` (wave 7): k8s/applications/temporal/db-secret-sync/
- `temporal` (wave 8): Helm chart + values
**After:**
- `temporal` (wave 8): Multi-source (Helm + values + PostSync db-secret-sync)
**Changes:**
- Added third source to temporal Application
- copy-job.yaml already has PostSync hook annotation
- Removed temporal-db-secret-sync Application from `60-applications.yaml`
---
### ✅ 4. Removed Duplicate: ingress-nginx
**Before:**
- `ingress-nginx-bootstrap` (bootstrap): Working, has LoadBalancer IP
- `ingress-nginx` (ArgoCD): Duplicate, LoadBalancer pending
**After:**
- `ingress-nginx-bootstrap` (bootstrap): Kept
**Changes:**
- Removed ingress-nginx Application from `00-substrate.yaml`
- Bootstrap version breaks circular dependency (ArgoCD → Forgejo → Ingress)
- Eliminated duplicate DaemonSet
---
## Skipped Consolidations
### ⏭️ cert-manager + cert-manager-issuers
**Decision:** KEEP SEPARATE
**Reasoning:**
- cert-manager (wave 0) installs CRDs
- cert-manager-issuers (wave 1) creates Issuers using those CRDs
- Wave separation ensures CRDs exist before Issuers
- Merging risks race condition (Issuer created before CRD ready)
- This is intentional separation for safety
---
## Files Modified
1. `k8s/bootstrap/ingress/kustomization.yaml` - Added resources list
2. `k8s/argocd/apps/00-substrate.yaml` - Merged wildcard-cert + homelab-ingress, removed ingress-nginx
3. `k8s/argocd/apps/60-applications.yaml` - Merged homarr + temporal Applications
---
## Benefits Achieved
### ✅ Easier Management
- 4 fewer Application CRs to track
- Related resources grouped together
- Clearer ownership model
### ✅ Better Sync Behavior
- PostSync hooks ensure proper ordering
- No separate Applications for patches/hooks
- Single Application manages entire stack
### ✅ Cleaner Structure
- Multi-source Applications are standard pattern
- Each logical service = one Application
- Hooks embedded where they belong
---
## Next Steps (Optional Phase 2)
### Potential Future Consolidations:
1. **SQS Platform** (5 → 2 Applications)
- Keep `strimzi-operator` separate
- Merge: kmsvc-redis + kafka-cluster + queue-crd + management-service
2. **IAM** (3 → 2 Applications)
- Keep `vault` separate
- Merge: authentik + iam-jobs
3. **Monitoring** (3 → 2 Applications)
- Keep `prometheus-crds` separate (wave 0)
- Merge: prometheus + blackbox-exporter
**Recommendation:** Evaluate after Forgejo push and observe current consolidations in action.
---
## Validation Plan
After pushing to Forgejo:
```bash
# Check Applications synced successfully
kubectl get applications -n argocd
# Verify consolidated Applications are healthy
kubectl get application ingress-config -n argocd
kubectl get application homarr -n argocd
kubectl get application temporal -n argocd
# Check ingress-nginx duplicate removed
kubectl get application ingress-nginx -n argocd # Should be gone
# Verify resources deployed correctly
kubectl get certificate -n ingress-nginx riotpiao-com-tls
kubectl get ingress -A
kubectl get job -n dashboard homarr-fix-probes
kubectl get job -n temporal temporal-db-secret-sync
```
---
## Rollback Procedure
If issues arise, revert specific commits:
```bash
# Identify commit
git log --oneline | grep consolidation
# Revert specific merge
git revert <commit-hash>
# Or restore old Applications from git history
git show <commit>:k8s/argocd/apps/00-substrate.yaml > temp.yaml
kubectl apply -f temp.yaml
```
---
## Conclusion
Successfully consolidated 4 Applications while maintaining:
- ✅ Proper sync wave ordering
- ✅ Hook execution timing
- ✅ Resource namespace separation
- ✅ GitOps best practices
**Status:** Ready to commit and push to Forgejo for validation.
-201
View File
@@ -1,201 +0,0 @@
# GitOps Migration Status - LIVE CLUSTER
**Migration completed:** 2026-07-23 23:13:04
**Status:****CORE INFRASTRUCTURE HEALTHY**
---
## ✅ What's Working (HEALTHY)
### **Infrastructure (100% Healthy)**
- **ArgoCD:** 4/4 pods running
- **Cert-Manager:** Deployed, ready
- **Ingress-Nginx:** Deployed, ready
- **Reloader:** Synced, healthy
### **Storage (3-Node HA Confirmed!)**
- **Longhorn nodes:** 3/3 ready ✅
- talos-cp-1: Ready (4d8h)
- talos-cp-2: Ready (14h)
- talos-cp-3: Ready (14h)
- **All volumes:** 17 volumes, all with 3 replicas ✅
- **DDB cluster:** 3/3 instances, "Cluster in healthy state" ✅
- ddb-cluster-1: 10Gi, 3 replicas
- ddb-cluster-2: 10Gi, 3 replicas
- ddb-cluster-3: 10Gi, 3 replicas
### **Monitoring & Logging**
- **Prometheus:** 3/3 pods running
- **Grafana:** 4/4 pods running (accessible at http://10.110.221.93)
- **Loki:** Deployed
- **Promtail:** Deployed
### **Services**
- **Forgejo:** LoadBalancer IP 192.168.1.165 (reinitializing after update)
- **Forgejo Redis:** 1/1 running
---
## ⚠️ Known Issues (Pre-Existing, Not Migration-Related)
### **1. Kafka/SQS Pods CrashLoopBackOff**
**Status:** Pre-existing (9h old)
**Impact:** Low (SQS service-specific issue)
**Action:** Not related to migration - investigate separately
```
kmsvc-entity-operator: CrashLoopBackOff (9h)
kmsvc-kmsvc-pool-0: CrashLoopBackOff (9h)
kmsvc-kmsvc-pool-2: CrashLoopBackOff (9h)
```
**Recommendation:** Check Kafka configuration separately
### **2. Authentik CreateContainerConfigError**
**Status:** Pre-existing (10h old)
**Impact:** Medium (IAM service affected)
**Action:** Secrets exist, may be configuration issue
**Recommendation:** Check authentik pod describe for specific error
### **3. Forgejo Reinitializing**
**Status:** Expected (after Application update)
**Impact:** Temporary (normal init process)
**Action:** Wait for init containers to complete (~2-5 minutes)
```
forgejo-gitea pods: Init:0/3 (normal)
```
**Recommendation:** Monitor, should auto-resolve
---
## 🎯 Migration Changes Applied
### **1. Data Schemas Path Updated**
- **Before:** `k8s/data` (included ddb-cluster - DUPLICATION)
- **After:** `k8s/data/schemas` (schemas only - no duplication)
- **Status:** ✅ Applied successfully
### **2. Longhorn 3-Node HA Verified**
- **Before:** CLAUDE.md claimed single-node storage
- **After:** Confirmed 3-node HA with 3 replicas per volume
- **Status:** ✅ Working perfectly
### **3. App-of-Apps Root Created**
- **Application:** homelab-root
- **Status:** ✅ Created and managing all child apps
### **4. Wave Structure**
- All applications organized in waves 0-9
- **Status:** ✅ Syncing properly
---
## 📊 Application Status Summary
**Total Applications:** 37
**Synced:** 3 (reloader, kmsvc-redis, strimzi-operator)
**OutOfSync:** 2 (cnpg-operator, forgejo) - being synced
**Unknown:** 32 (normal during reconciliation)
**Degraded:** 1 (kafka-cluster) - pre-existing issue
**Expected:** All apps will transition to "Synced" within 5-10 minutes as ArgoCD reconciles.
---
## 🚀 Next Steps
### **Immediate (Next 5 minutes)**
1. **Monitor Forgejo initialization:**
```bash
watch kubectl get pods -n cicd
```
Wait for forgejo-gitea pods to show `Running` (currently in `Init:0/3`)
2. **Check ArgoCD sync status:**
```bash
watch kubectl get applications -n argocd
```
Most apps should transition from `Unknown` → `Synced`
### **Short-term (Next hour)**
3. **Investigate pre-existing issues:**
- Kafka CrashLoopBackOff (not migration-related)
- Authentik CreateContainerConfigError (not migration-related)
4. **Verify all services accessible:**
```bash
# Test ingress
curl -k https://forgejo.riotpiao.com
curl -k https://grafana.riotpiao.com
curl -k https://argocd.riotpiao.com
```
5. **Update CLAUDE.md:**
- Change topology table to reflect 3-node HA storage
- Update hard rule about node renaming (all 3 nodes, not just cp-1)
### **Documentation Updates**
6. **Create final migration summary:**
- Document what was changed
- Note pre-existing issues
- Update cluster architecture docs
---
## 🎉 Migration Success Criteria
| Criterion | Status |
|-----------|--------|
| Zero downtime | ✅ Achieved |
| No data loss | ✅ Confirmed (DDB cluster healthy) |
| Storage HA verified | ✅ 3 nodes, 3 replicas |
| ArgoCD managing all apps | ✅ 37 applications |
| GitOps workflow functional | ✅ Can sync via git push |
| Core services running | ✅ ArgoCD, DDB, Forgejo, monitoring |
---
## 📝 Commands Reference
### **Check overall health:**
```bash
kubectl get applications -n argocd
kubectl get pods --all-namespaces | grep -v Running | grep -v Completed
kubectl get nodes.longhorn.io -n longhorn-system
kubectl get cluster -n ddb
```
### **Force sync specific app:**
```bash
kubectl patch application <app-name> -n argocd --type=merge \
-p='{"operation":{"initiatedBy":{"username":"manual"},"sync":{"prune":true}}}'
```
### **Check logs:**
```bash
kubectl logs -n argocd deployment/argocd-application-controller --tail=50
kubectl logs -n argocd deployment/argocd-repo-server --tail=50
```
---
## ✅ Conclusion
**The migration is SUCCESSFUL!**
- Core infrastructure is fully operational
- 3-node HA storage confirmed (17 volumes with 3 replicas each)
- DDB cluster healthy (3/3 instances)
- ArgoCD managing all 37 applications
- GitOps workflow ready (future changes via git push)
**Pre-existing issues** (Kafka, Authentik) are unrelated to the migration and should be investigated separately.
**Your cluster is LIVE and fully operational!** 🚀
-475
View File
@@ -1,475 +0,0 @@
# GitOps Infrastructure Review - Summary
**Date:** 2025-01-XX
**Objective:** Review entire k8s GitOps infrastructure, eliminate duplication, create bootstrap-from-local + GitOps-managed-future workflow.
---
## 📌 What Was Done
### 1. **Audit of Current State**
**Findings:**
- ✅ App-of-apps pattern correctly implemented
- ✅ Wave-based deployment (0-8) mostly correct
- ⚠️ **Resource duplication:** `ddb-cluster.yaml` in both bootstrap and GitOps paths
- ⚠️ **Circular dependency:** Forgejo hosts repo → ArgoCD syncs from repo → Forgejo needs DB
- ⚠️ **Manual bootstrap steps:** Scattered, error-prone
- ⚠️ **SOPS secrets:** Some excluded from kustomization (out-of-band)
- ⚠️ **No single source of truth:** Same resources in multiple places
### 2. **Created Bootstrap-Local Bundle**
**New files created:**
```
k8s/bootstrap-local/
├── kustomization.yaml # Orchestrates bootstrap
├── 00-namespaces.yaml # All namespaces with PodSecurity labels
├── 01-argocd.yaml # ArgoCD ConfigMaps
├── 02-cnpg-operator.yaml # CNPG operator Application
├── 03-ddb-bootstrap.yaml # PostgreSQL cluster + Forgejo DB + Redis
└── 04-forgejo.yaml # Forgejo Application (inline values)
```
**What it does:**
- Apply once from local checkout (`kubectl apply -k k8s/bootstrap-local/`)
- Creates ArgoCD, CNPG, DDB, Forgejo (everything needed for GitOps)
- No git dependency (chicken-egg problem solved)
- Idempotent (safe to re-run)
### 3. **Restructured k8s/data/ to Eliminate Duplication**
**Before:**
```
k8s/data/
├── ddb-cluster.yaml # ❌ Deployed by data-schemas app (wrong)
├── forgejo-database.yaml # ❌ Deployed by data-schemas app (wrong)
├── authentik-database.yaml
├── temporal-database.yaml
├── ...
└── kustomization.yaml # Listed ALL resources
```
**After:**
```
k8s/data/
├── cluster/ # 🔴 Bootstrap-only (not in GitOps)
│ ├── ddb-cluster.yaml
│ ├── forgejo-database.yaml
│ └── kustomization.yaml # Reference only
└── schemas/ # 🟢 GitOps-managed (wave 6)
├── authentik-database.yaml
├── temporal-database.yaml
├── schemas.yaml
├── db-init-job.yaml
└── kustomization.yaml
```
**Result:** Each resource has exactly ONE source of truth.
### 4. **Updated ArgoCD Apps to Avoid Duplication**
**Modified:**
- `k8s/argocd/apps/40-data.yaml` → Points to `k8s/data/schemas/` (NOT `k8s/data/`)
- Wave renumbered for clarity (0-8 sequential)
**Deleted:**
- Old `k8s/data/kustomization.yaml` (replaced by subdirectories)
### 5. **DDB Configuration Review**
**Created:** `DDB-REVIEW.md` with:
- Current configuration analysis
- Recommendations (resource limits, connection pooling, backups, monitoring)
- Proposed enhanced configuration
- Migration path
**Key recommendations:**
- Increase `shared_buffers` 256MB → 512MB (multi-tenant workload)
- Add resource limits (CPU/memory)
- Enable PgBouncer pooler (Temporal has high connection count)
- Configure backups to MinIO
- Enable `enablePodMonitor: true` for Prometheus
### 6. **Automated Bootstrap Script**
**Created:** `bootstrap.sh` with:
- Preflight checks (kubectl, SOPS key, ArgoCD CLI)
- ArgoCD installation
- SOPS age secret creation (never in git)
- Bootstrap bundle application
- Wait loops for each component (CNPG, DDB, Forgejo)
- Secret copying (ddb to cicd namespace)
- Clear next-steps instructions
**Usage:**
```bash
./bootstrap.sh
# ✅ ArgoCD + CNPG + DDB + Forgejo ready in <10 minutes
```
### 7. **Comprehensive Documentation**
**Created:**
1. **GITOPS-REBUILD-PLAN.md** (15KB)
- Current vs proposed architecture diagrams
- Complete directory structure (single source of truth)
- Step-by-step execution plan
- Resource mapping table (no duplication)
- Testing procedures
- Day-2 operations guide
- FAQ
2. **DDB-REVIEW.md** (6KB)
- PostgreSQL configuration review
- Performance tuning recommendations
- Backup/monitoring setup
- Enhanced configuration example
3. **IMPLEMENTATION-CHECKLIST.md** (9KB)
- Pre-implementation tasks
- Two migration paths (fresh cluster vs incremental)
- Post-implementation verification (wave-by-wave)
- Cleanup steps
- Day-2 validation tests
- Rollback procedures
4. **REVIEW-SUMMARY.md** (this file)
- High-level overview
- Key decisions explained
- What to review next
---
## 🎯 Key Decisions Made
### **Decision 1: DDB Cluster Stays Bootstrap-Only**
**Rationale:**
- Circular dependency: Forgejo needs DDB → ArgoCD syncs from Forgejo → Cannot bootstrap DDB from git
- **Solution:** DDB deployed via `bootstrap-local/03-ddb-bootstrap.yaml`, never touched by ArgoCD
- **Trade-off:** DDB changes require manual `kubectl apply` (but still committed to git for record-keeping)
- **Alternative:** If Forgejo moves to external git host (GitHub), DDB could become GitOps-managed
**ArgoCD Application for DDB:** Not created (would duplicate bootstrap). If needed later (external git), see `GITOPS-REBUILD-PLAN.md` for example.
### **Decision 2: Forgejo Manual-Sync-Only**
**Rationale:**
- Forgejo hosts the repo CI pushes to
- Auto-sync would let a bad CI commit break the system CI depends on
- **Solution:** `syncPolicy.automated: {}` (manual sync only)
- Application exists (`04-forgejo.yaml`) but never auto-syncs
### **Decision 3: Wave Renumbering (0-8 Sequential)**
**Before:** 00, 05, 10, 20, 30, 40, 50, 60 (helmfile convention, gaps for insertion)
**After:** 0, 1, 2, 3, 4, 5, 6, 7, 8 (ArgoCD native, cleaner)
**Rationale:**
- ArgoCD sync-wave already handles ordering
- No need for gaps (can insert 2.5 if needed, or renumber)
- Easier to read/understand
### **Decision 4: Bootstrap Script Over Manual Steps**
**Before:** 30+ manual commands in BOOTSTRAP.md
**After:** Single `./bootstrap.sh` script
**Rationale:**
- Reduces human error
- Idempotent (safe to re-run)
- Self-documenting (script IS the procedure)
- Faster iteration (cluster rebuild in <10 min)
### **Decision 5: Separate k8s/data/cluster/ from k8s/data/schemas/**
**Rationale:**
- Clear separation: bootstrap vs GitOps
- Prevents accidental deletion of cluster by ArgoCD prune
- Each directory has its own kustomization.yaml (no ambiguity)
- Easier to reason about dependencies
---
## 📂 Files Created/Modified
### **Created (New Files)**
```
k8s/bootstrap-local/
kustomization.yaml
00-namespaces.yaml
01-argocd.yaml
02-cnpg-operator.yaml
03-ddb-bootstrap.yaml
04-forgejo.yaml
k8s/data/cluster/
kustomization.yaml
k8s/data/schemas/
kustomization.yaml
bootstrap.sh
GITOPS-REBUILD-PLAN.md
DDB-REVIEW.md
IMPLEMENTATION-CHECKLIST.md
REVIEW-SUMMARY.md (this file)
```
### **Modified (Updated Files)**
```
k8s/argocd/apps/40-data.yaml
- Changed path: k8s/data → k8s/data/schemas
- Changed sync-wave: 4 → 6
- Updated comments
```
### **Deleted**
```
k8s/data/kustomization.yaml (replaced by subdirectories)
```
### **Moved**
```
k8s/data/ddb-cluster.yaml → k8s/data/cluster/ddb-cluster.yaml
k8s/data/forgejo-database.yaml → k8s/data/cluster/forgejo-database.yaml
k8s/data/authentik-database.yaml → k8s/data/schemas/authentik-database.yaml
k8s/data/temporal-database.yaml → k8s/data/schemas/temporal-database.yaml
k8s/data/temporal-visibility-database.yaml → k8s/data/schemas/temporal-visibility-database.yaml
k8s/data/schemas.yaml → k8s/data/schemas/schemas.yaml
k8s/data/db-init-job.yaml → k8s/data/schemas/db-init-job.yaml
```
---
## 🔍 What You Should Review
### **1. Bootstrap Script**
**File:** `bootstrap.sh`
**Review for:**
- [ ] SOPS key path (`~/.sops/homelab-age.key` correct?)
- [ ] ArgoCD installation method (manifest URL vs Helm?)
- [ ] Wait timeout values (300s, 600s reasonable?)
- [ ] Error handling (should script exit or continue?)
### **2. DDB Configuration**
**File:** `DDB-REVIEW.md`
**Decide:**
- [ ] Accept current config (256MB shared_buffers, no backups)?
- [ ] Implement enhanced config (512MB, PgBouncer, S3 backups)?
- [ ] When to apply changes (now vs after migration)?
**If implementing enhanced config:**
1. Create MinIO bucket `ddb-backups`
2. Create `ddb-backup-s3` secret
3. Update `k8s/bootstrap-local/03-ddb-bootstrap.yaml` with enhanced spec
4. Test on staging cluster first
### **3. Wave Structure**
**Files:** `k8s/argocd/apps/*.yaml`
**Verify:**
- [ ] Wave ordering correct? (0=substrate, 1=networking, ..., 8=apps)
- [ ] Dependencies satisfied? (e.g., schemas after secrets)
- [ ] Sync policies appropriate? (automated vs manual)
**Current wave structure:**
```
Wave 0: cert-manager, ingress-nginx, reloader, CNPG operator
Wave 1: Cilium policies, CoreDNS (networking)
Wave 2: MinIO, Longhorn, Prometheus (storage/observability)
Wave 3: Loki, Grafana, Promtail (logging)
Wave 4: SOPS secrets (all *.enc.yaml)
Wave 5: Vault, Authentik, Forgejo runner (IAM)
Wave 6: Database schemas (authentik-db, temporal-db, etc.)
Wave 7: Kafka, Redis, SQS (messaging)
Wave 8: Temporal, Portainer, cloudflared, etc. (applications)
```
### **4. Namespace Labels**
**File:** `k8s/bootstrap-local/00-namespaces.yaml`
**Verify PodSecurity labels correct:**
- [ ] `cicd` = privileged (Forgejo runner needs DinD)
- [ ] `ingress-nginx` = privileged (hostPort 80/443)
- [ ] `monitoring` = privileged (node-exporter hostPath)
- [ ] `logging` = privileged (promtail hostPath)
- [ ] All others = baseline (default)?
### **5. Forgejo Configuration**
**File:** `k8s/bootstrap-local/04-forgejo.yaml`
**Verify inline values match:**
- [ ] Admin username/email correct?
- [ ] Domain `forgejo.riotpiao.com` correct?
- [ ] LoadBalancer IP `192.168.1.165` available?
- [ ] OAuth2 config matches Authentik setup?
- [ ] Redis connection string correct?
**Sync with:** `k8s/security/ci-cd/forgejo-values.yaml` (keep both files in sync per comment)
### **6. SOPS Secrets**
**File:** `k8s/argocd/apps/04-secrets.yaml` (check if exists)
**Verify:**
- [ ] SOPS plugin configured correctly?
- [ ] All `.enc.yaml` files decrypted successfully?
- [ ] `db-role-secrets.enc.yaml` applied before wave 6?
**Check these secrets exist after bootstrap:**
```bash
kubectl get secret -n ddb authentik-db-role
kubectl get secret -n ddb temporal-db-role
kubectl get secret -n cicd ddb-cluster-app # Copied from ddb namespace
kubectl get secret -n argocd sops-age
```
---
## 🚀 Next Steps (Recommended Order)
1. **Review all documents** (this file, GITOPS-REBUILD-PLAN.md, DDB-REVIEW.md)
2. **Decide on DDB config** (current vs enhanced)
3. **Review bootstrap.sh** and customize if needed
4. **Test on staging cluster first** (if available)
5. **Backup current production state** (PVCs, secrets)
6. **Choose migration path:**
- **Option A:** Fresh cluster rebuild (faster, cleaner)
- **Option B:** Incremental migration (safer, slower)
7. **Follow IMPLEMENTATION-CHECKLIST.md** step-by-step
8. **Validate each wave** before proceeding to next
9. **Document any issues** encountered
10. **Update CLAUDE.md** after successful migration
---
## ⚠️ Important Notes
### **Before You Start**
1. **Backup PVCs** (Forgejo git repos, PostgreSQL data)
- Longhorn snapshots or Velero backup
- Export critical data (Forgejo repos, Authentik config)
2. **Test SOPS key** works
```bash
export SOPS_AGE_KEY_FILE=~/.sops/homelab-age.key
sops -d k8s/data/db-role-secrets.enc.yaml
# Should decrypt successfully
```
3. **Verify Talos cluster healthy**
```bash
talosctl health --nodes <all-nodes>
kubectl get nodes
# All Ready
```
### **During Implementation**
- **Go wave-by-wave** - Don't skip verification steps
- **Watch ArgoCD UI** - https://argocd.riotpiao.com
- **Check logs** if any app stuck:
```bash
kubectl logs -n argocd deployment/argocd-application-controller
kubectl logs -n argocd deployment/argocd-repo-server
```
### **After Implementation**
- **Test GitOps workflow** (make a change, push, verify auto-sync)
- **Test rollback** (git revert, verify auto-sync)
- **Document any deviations** from plan
- **Update runbooks** based on lessons learned
---
## 📊 Resource Duplication Check (Final)
**Bootstrap-only resources (NOT in ArgoCD GitOps):**
- ArgoCD itself
- CNPG operator (deployed as Application in bootstrap, but manual-managed)
- ddb-cluster
- forgejo-database
- Forgejo (exists as Application but manual-sync-only)
- Forgejo Redis
**GitOps-managed resources (ArgoCD auto-syncs):**
- cert-manager, ingress-nginx, reloader
- Cilium policies, CoreDNS config
- MinIO, Longhorn config, Prometheus
- Loki, Grafana, Promtail
- SOPS secrets
- Vault, Authentik, Forgejo runner
- Database schemas (authentik-db, temporal-db, etc.)
- Kafka, Redis, SQS
- Temporal, Portainer, cloudflared, etc.
**Terraform-managed resources:**
- Talos machine configs (controlplane.tftpl)
- No k8s resources
**✅ No overlap - each resource has exactly ONE source of truth.**
---
## 🎯 Success Criteria
After successful implementation, you should have:
- [x] **Single command bootstrap** (`./bootstrap.sh`)
- [x] **Zero manual kubectl apply** (except bootstrap)
- [x] **Git is source of truth** (all changes via push)
- [x] **No resource duplication**
- [x] **Clear wave ordering** (0-8)
- [x] **Fast iteration** (cluster rebuild <10 min)
- [x] **Rollback via git** (revert commit, auto-syncs)
- [x] **Well-documented** (5 comprehensive docs)
---
## 📞 Questions to Resolve
Before implementation, decide on:
1. **Fresh cluster vs incremental migration?**
- Fresh = faster, cleaner (requires downtime)
- Incremental = safer, slower (zero downtime possible)
2. **DDB enhanced config now or later?**
- Now = better performance from start
- Later = faster migration, can optimize afterward
3. **Wave renumbering (00→0, 05→1, etc.)?**
- Yes = cleaner, consistent with plan
- No = keep current, less churn
4. **Delete old k8s/argocd/bootstrap/ after migration?**
- Yes = cleaner repo
- No = keep for reference
5. **Update CLAUDE.md immediately or after validation?**
- Immediately = stays current
- After = confirms plan actually works
---
**Ready to proceed?** Start with `IMPLEMENTATION-CHECKLIST.md` and check off each step.
**Need clarification?** Review specific sections in `GITOPS-REBUILD-PLAN.md`.
**Performance tuning?** See `DDB-REVIEW.md` for PostgreSQL optimization.
-304
View File
@@ -1,304 +0,0 @@
# Storage Architecture Clarification
**Issue:** CLAUDE.md contradicts actual Longhorn configuration manifests.
---
## 🚨 Contradiction Found
### CLAUDE.md States:
```
| Node | Storage |
|------|---------|
| talos-cp-1 (.213) | sole Longhorn node |
| talos-cp-2 (.163) | none |
| talos-cp-3 (.166) | none |
"Only talos-cp-1 runs workloads and holds storage →
stateful services are single-instance."
```
### Actual Longhorn Manifests Show:
**1. Explicit Node CRDs for ALL 3 nodes:**
```yaml
# k8s/infrastructure/longhorn/longhorn-nodes.yaml
---
apiVersion: longhorn.io/v1beta2
kind: Node
metadata:
name: talos-cp-2
spec:
allowScheduling: true # ← Storage enabled!
disks:
default-disk:
allowScheduling: true
path: /var/lib/longhorn
---
apiVersion: longhorn.io/v1beta2
kind: Node
metadata:
name: talos-cp-3
spec:
allowScheduling: true # ← Storage enabled!
disks:
default-disk:
allowScheduling: true
path: /var/lib/longhorn
```
**2. Taint toleration for control-plane:**
```yaml
# longhorn-taint-toleration.yaml
value: "node-role.kubernetes.io/control-plane:NoSchedule"
# Allows Longhorn DaemonSet on ALL control-plane nodes
```
**3. StorageClass with 3 replicas:**
```yaml
# longhorn-wffc-storageclass.yaml
parameters:
numberOfReplicas: "3" # ← 3-way replication!
volumeBindingMode: WaitForFirstConsumer
```
**4. PostSync job to expand existing volumes:**
```yaml
# expand-replicas-job.yaml
# Patches ALL volumes from 1 → 3 replicas
```
---
## 🔍 What's the Truth?
**Need to verify cluster state:**
```bash
# Check Longhorn nodes
kubectl get nodes.longhorn.io -n longhorn-system -o wide
# Expected output (if 3-node setup is actually working):
# NAME READY ALLOWSCHEDULING SCHEDULABLE AGE
# talos-cp-1 True true true Xd
# talos-cp-2 True true true Xd
# talos-cp-3 True true true Xd
# Check actual replica counts
kubectl get volumes.longhorn.io -n longhorn-system \
-o custom-columns='NAME:.metadata.name,REPLICAS:.spec.numberOfReplicas,STATE:.status.state'
# Check DDB PVCs
kubectl get pvc -n ddb
kubectl describe pvc <pvc-name> -n ddb | grep -A 5 "Volumes:"
```
---
## 📊 Two Possible Scenarios
### **Scenario A: 3-Node Replication is Active** ✅
**If the Longhorn manifests are actually deployed:**
```
Storage Architecture:
┌─────────────────────────────────────────────────┐
│ DDB PVC (10Gi, Longhorn) │
├─────────────────────────────────────────────────┤
│ Replica 1: talos-cp-1:/var/lib/longhorn │
│ Replica 2: talos-cp-2:/var/lib/longhorn │
│ Replica 3: talos-cp-3:/var/lib/longhorn │
└─────────────────────────────────────────────────┘
DDB PostgreSQL Pods:
┌──────────────┬──────────────┬──────────────┐
│ ddb-cluster-1│ ddb-cluster-2│ ddb-cluster-3│
│ (cp-1) │ (cp-2) │ (cp-3) │
│ Primary │ Replica │ Replica │
└──────────────┴──────────────┴──────────────┘
↓ ↓ ↓
Reads all 3 Longhorn replicas locally
(dataLocality: best-effort)
Failure Scenarios:
❌ cp-1 fails → Replica 2 & 3 still available
❌ cp-2 fails → Replica 1 & 3 still available
❌ cp-3 fails → Replica 1 & 2 still available
✅ Data survives ANY single node failure
```
**This is TRUE HA storage!**
### **Scenario B: CLAUDE.md is Correct** ❌
**If Longhorn manifests are NOT actually deployed:**
```
Storage Architecture:
┌─────────────────────────────────────────────────┐
│ DDB PVC (10Gi, Longhorn) │
├─────────────────────────────────────────────────┤
│ Replica 1: talos-cp-1:/var/lib/longhorn │
│ (NO replicas on cp-2, cp-3) │
└─────────────────────────────────────────────────┘
DDB PostgreSQL Pods:
┌──────────────┬──────────────┬──────────────┐
│ ddb-cluster-1│ ddb-cluster-2│ ddb-cluster-3│
│ (cp-1) │ (cp-2) │ (cp-3) │
│ Primary │ Replica │ Replica │
└──────────────┴──────────────┴──────────────┘
↓ ↓ ↓
ALL pods must read from cp-1 over network
(single point of failure)
Failure Scenarios:
❌ cp-1 disk fails → PERMANENT DATA LOSS
❌ cp-1 node fails → All PVCs inaccessible
❌ NO HA for storage at all
```
**This is NOT HA storage!**
---
## 🎯 Action Required: Verify Cluster State
**Run these commands to determine which scenario is true:**
```bash
# 1. Check if Longhorn Node CRs exist
kubectl get nodes.longhorn.io -n longhorn-system
# 2. Check if taint toleration is set
kubectl get setting taint-toleration -n longhorn-system -o yaml
# 3. Check actual volume replica counts
kubectl get volumes.longhorn.io -n longhorn-system \
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.numberOfReplicas}{"\n"}{end}'
# 4. Check DDB PVC details
kubectl get pvc -n ddb -o yaml | grep -A 10 "volumeName:"
# 5. Check Longhorn DaemonSet pods
kubectl get pods -n longhorn-system -o wide | grep longhorn-manager
# Should show pods on ALL 3 nodes if 3-node setup is active
```
---
## 🔧 If Scenario B (Single Node) is True
**You need to deploy the Longhorn HA configuration:**
```bash
# Apply the Longhorn HA manifests
kubectl apply -k k8s/infrastructure/longhorn/
# This will:
# 1. Create Node CRs for cp-2, cp-3
# 2. Set taint toleration
# 3. Create 3-replica StorageClass
# 4. Run PostSync job to expand existing volumes
# Verify expansion happened
kubectl get job longhorn-expand-replicas -n longhorn-system
kubectl logs job/longhorn-expand-replicas -n longhorn-system
```
---
## 📝 Corrected Documentation
**If 3-node replication IS active, update CLAUDE.md:**
```diff
| Node | IP | Zone | Scheduling | Storage |
|------|----|----|-----------|---------|
-| `talos-cp-1` | .213 | az-a | schedulable (all workloads) | sole Longhorn node |
-| `talos-cp-2` | .163 | az-b | dedicated (`NoSchedule`) | none |
-| `talos-cp-3` | .166 | az-c | dedicated (`NoSchedule`) | none |
+| `talos-cp-1` | .213 | az-a | schedulable (all workloads) | Longhorn (replica 1/3) |
+| `talos-cp-2` | .163 | az-b | dedicated (`NoSchedule`) | Longhorn (replica 2/3) |
+| `talos-cp-3` | .166 | az-c | dedicated (`NoSchedule`) | Longhorn (replica 3/3) |
-holds storage → stateful services are single-instance.
+holds storage → stateful services are HA (3-replica volumes).
```
**And update the hard rule:**
```diff
-🔴 **NEVER rename or wipe `talos-cp-1` (.213).** It is the sole Longhorn storage
-node — all replicas are pinned to that node name. Renaming orphans its Longhorn
-node CR and faults every volume (permanent data loss).
+🔴 **NEVER rename ANY control-plane node.** Longhorn volumes have 3 replicas
+pinned to specific node names (talos-cp-1, talos-cp-2, talos-cp-3). Renaming
+ANY node orphans its Longhorn Node CR and degrades all volumes. Loss of 2+ nodes
+simultaneously = permanent data loss.
```
---
## 🎯 Impact on DDB Configuration
**If 3-node replication is active:**
### DDB cluster is actually HA! ✅
```
Compute HA: 3 PostgreSQL pods across 3 nodes ✅
Storage HA: 3 Longhorn replicas across 3 nodes ✅
Network HA: 3 etcd members, Cilium IPAM ✅
Failure tolerance:
- 1 node failure: Cluster continues (2/3 quorum)
- 1 disk failure: Data intact (2/3 replicas)
- 2 nodes fail: ❌ Etcd loses quorum, data degrades
```
**Current DDB config is acceptable if 3-replica storage is confirmed.**
### If Single-Node Storage:
**You MUST either:**
1. **Deploy Longhorn 3-node config** (recommended)
```bash
kubectl apply -k k8s/infrastructure/longhorn/
```
2. **Reduce DDB to 1 instance** (match storage reality)
```yaml
# k8s/data/cluster/ddb-cluster.yaml
instances: 1 # Single instance if single-node storage
```
3. **Add external backup** (mitigate single-node risk)
```yaml
# DDB backup to MinIO (see DDB-REVIEW.md)
backup:
barmanObjectStore:
destinationPath: s3://ddb-backups/
```
---
## ✅ Recommended Actions (Priority Order)
1. **[ ] Verify cluster state** (run commands above)
2. **[ ] If single-node storage:** Deploy Longhorn 3-node config
3. **[ ] Wait for replicas to expand** (watch Longhorn UI)
4. **[ ] Update CLAUDE.md** with correct topology
5. **[ ] Update DDB-REVIEW.md** to reflect actual HA status
6. **[ ] Update GITOPS-REBUILD-PLAN.md** storage section
7. **[ ] Document findings** in TROUBLESHOOTING.md
---
**Next:** Please run the verification commands and report back:
- Are there 3 Longhorn nodes?
- What are the actual replica counts on DDB PVCs?
- Is the expand-replicas job present/completed?
+218
View File
@@ -0,0 +1,218 @@
# ADR 0001 — GitOps Bootstrap, CD, and Coverage
- **Status:** Proposed
- **Date:** 2026-07-23
- **Deciders:** Homelab platform owner
- **Context tags:** bootstrap, gitops, argocd, forgejo, cicd, supply-chain, disaster-recovery
One delivery system, three coupled parts:
- **Part A — Bootstrap:** bare cluster → self-hosted GitOps control plane (breaks the ArgoCD ↔ Forgejo circle).
- **Part B — Steady-state CD:** how app changes flow to the cluster once it's up.
- **Part C — Coverage & organization:** every resource captured by exactly one Application; no orphans, no duplicates.
Single 3-control-plane Talos cluster. GitOps via ArgoCD (app-of-apps under `k8s/argocd/apps/`, waves 08); CI via Forgejo Actions on a self-hosted runner.
---
## Part A — Bootstrap
### Problem — the circle
```
ArgoCD ──syncs from──▶ git repo ──hosted by──▶ Forgejo ──deployed by──▶ (the repo)
```
Forgejo hosts the repo ArgoCD reconciles. Today the circle is broken by installing Forgejo + Longhorn + CNPG **by hand** (`bootstrap-local/`, `bootstrap/phase1-5/`) — imperative, off-git, never reconciled, human-ordered rebuild.
### Decision — GitHub seed + Forgejo pull-mirror + cutover
GitHub is the seed and permanent DR source; Forgejo is steady-state truth after cutover and stays synced via a native pull-mirror.
**No adoption.** Longhorn, CNPG, and Forgejo are **pure Phase-0 manual bootstrap** — ArgoCD never manages them (this is the deliberate divergence from the "ArgoCD adopts hand-installed infra" pattern). They must exist *before* ArgoCD can sync from a git host at all, so making them Apps would re-introduce the circle. ArgoCD (seeded from GitHub) deploys only everything *after* the control plane.
```
Phase 0 (manual, once — the whole control plane):
Talos ─ Cilium CNI ─ Longhorn ─ CNPG operator ─ forgejo-db Cluster (wait Ready)
─ Forgejo + Redis ─ push repo ─ helm install argocd ─ apply root-app→GitHub
Phase 1 (ArgoCD from GitHub seed): waves 08 = monitoring · logging · iam · messaging ·
apps. Does NOT manage Longhorn / CNPG / Forgejo (those stay bootstrap-owned).
Phase 2 (cutover, once): flip root-app repoURL GitHub→Forgejo; Forgejo pull-mirrors
GitHub (live DR seed).
```
Manual order matters: CNPG operator + `forgejo-db` Cluster must be **Ready** (`.status.phase: "Cluster in healthy state"`, not just created) before `helm install forgejo`. Wait on it. `forgejo-db` = dedicated CNPG Cluster (`owner: forgejo`), preferred anti-affinity + control-plane tolerations so it can't deadlock on a partly-schedulable 3-CP.
**Options:** (A) one-time seed + manual cutover — mirror drifts. (B) **seed + pull-mirror + cutover — chosen** — safe cutover, always-current DR. (C) external-permanent — no self-hosted control plane. Rejected.
### Repository credentials (private GitHub seed)
Two read-only creds, neither needs write:
1. **ArgoCD → GitHub: SSH deploy key.** `ssh-keygen -t ed25519 -f argocd_seed -N ""`; add public key to repo → Deploy keys (read-only). Private key → ArgoCD repo-credential Secret at Phase 0 (before any secret controller):
```yaml
apiVersion: v1
kind: Secret
metadata:
name: seed-github-repo
namespace: argocd
labels: { argocd.argoproj.io/secret-type: repository }
stringData:
type: git
url: [email protected]:<org>/homelab.git # SSH; must match root-app repoURL
sshPrivateKey: |
-----BEGIN OPENSSH PRIVATE KEY-----
...
```
2. **Forgejo → GitHub: fine-grained PAT (Contents: Read).** HTTPS token for the pull-mirror; SOPS-encrypted in-repo (consumed at w1 once SOPS plugin is up).
Rotation: replace GitHub-side key/token, re-apply Secret / re-encrypt SOPS. Cutover keeps both (mirror stays live).
---
## Part B — Steady-state CD
**CI does build + push + sign only.** Dedicated controllers own promotion, reconciliation, safe rollout. Git = single source; nothing mutates cluster or CD repo by string-replace.
### Current problems
1. **`sed` image-bump** (`ci.yml:72`) — fragile, non-atomic, races, `git revert`-only rollback. "CI mutates CD repo" anti-pattern.
2. **Registry hostname split-brain** — one Forgejo addressed 4 ways; in-cluster pods pull via public ingress name (nginx hairpin, CLAUDE.md trap). `api` defaults to `:latest`.
3. **Fragmented truth** — `project: default` vs `homelab`; `api` App points at external `rock/deploy.git`.
4. **DinD runner** — privileged; RWO cache pins it to cp-1 (SPOF).
5. **No progressive delivery / supply-chain gate** — all `prune+selfHeal`, no canary/analysis/rollback, unsigned images.
### Decision
| Concern | From | To |
|---|---|---|
| Image promotion | CI `sed` → deploy repo | **ArgoCD Image Updater**, git write-back, digest-pinned |
| Build | DinD (privileged, cp-1) | **rootless BuildKit** (or Kaniko) + registry cache |
| Registry addressing | 4 hostnames | in-cluster `*.svc.cluster.local:3000`; one external push name |
| Delivery | raw auto-sync | **Argo Rollouts** canary + Prometheus `AnalysisTemplate` (app tier) |
| Supply chain | none | **cosign sign + Kyverno `verifyImages`** |
| Success signal | pod `Running` | **Playwright E2E** (video/trace) gate |
| Projects/repos | `default`+`homelab`, 2 repos | one `homelab` project, one convention |
```
push→main → runner(BuildKit): test · build·push <in-cluster-registry>/<repo>@sha256 · cosign sign
→ Image Updater watches registry, writes digest back to git
→ app-of-apps: infra/platform = plain sync ; app tier = Rollouts canary + Kyverno gate
```
**Options:** promotion — Image Updater (chosen) over `kustomize edit` (still coupled) / Flux (2nd engine). Build — BuildKit (chosen) over DinD / Kaniko. Delivery — Rollouts app-tier-only (chosen) over none / Flagger (needs mesh).
### Deployment verification (synthetic E2E)
`Running` proves scheduling, not the user path (ingress/TLS/OIDC/session/UI). Signal ladder: readiness → synthetic HTTP → **Playwright browser E2E** (gate on the top rung). Suite hits **public ingress hostnames**, records video+trace → MinIO `e2e-artifacts/` (private, lifecycle-expired). Journeys:
- **Portainer** — login page renders.
- **Authentik** — sign in (bootstrap `akadmin` from secret store), dashboard loads.
- **OAuth/OIDC federation** — each app (ArgoCD, Forgejo, Grafana…): click SSO → complete Authentik → land back authenticated. Catches client mis-registration / redirect-URI / broken provider that a green pod hides.
Runs two ways: **deploy gate** (Rollouts `AnalysisTemplate` / PostSync Job → fails sync/rollout) and **continuous smoke** (`CronJob` → Alertmanager). Creds from SOPS/Vault as env, never hardcoded. Constraint: **WebKit only** (Safari engine) per requirement. Suite lives at `tests/e2e/`; manifests at `k8s/platform/e2e/` (unwired until phase 7).
---
## Part C — GitOps coverage & organization
**Principle: every deployable directory is the source of exactly one Application, grouped by wave.** No dead aggregator kustomizations, no orphan manifests, no second app-of-apps. Layer folders stay; the tree is reconciled to the map. An App uses a git **`path:`** for manifests/kustomize bases, a **`$values` valueFile** when it only parameterizes a remote Helm chart. Secrets are the exception — one `sops-secrets` App globs all `*.enc.yaml` from repo root.
### Folder ↔ Application map (as-built)
Consolidated to two folders — `k8s/infra` (platform/infra tier) and `k8s/apps`
(workloads) — replacing the old `infrastructure/platform/security/applications`
layer folders (deleted).
```
k8s/
├── argocd/ root/ + apps/ (waves) + projects/ # engine, not an App target
├── bootstrap/ cert-manager w1 · cilium w0 · coredns w0 · ingress w1 # Phase-0 substrate
├── infra/
│ ├── longhorn/ →longhorn-config w1
│ ├── minio/ →minio-tenant (+minio-operator-values for operator) w1
│ ├── monitoring/ →monitoring-config w2 · crds/ →prometheus-crds w0
│ │ *-values →prometheus/blackbox w1
│ ├── logging/*-values →grafana/loki/promtail w2
│ ├── data-schemas/ →data-schemas w6 (CNPG operator + forgejo-db → Phase-0 manual bootstrap, NOT an App)
│ ├── iam/ →iam-jobs w3 (+authentik/vault values)
│ ├── forgejo-runner/ →forgejo-runner w3
│ └── cicd/ →cicd-config w3 (Forgejo OAuth job)
└── apps/
├── cloudflared/ duckdns/ homarr/ portainer/ temporal/ →w8
└── messaging/{kafka-cluster,queue-crd,management-service} →w57
```
### Orphans — declared in git, reconciled by NOTHING
| Path | Count | Kind | Action |
|---|---|---|---|
| `platform/monitoring/{alerts,servicemonitors,dashboards}` | 27 | PrometheusRule / ServiceMonitor / dashboard ConfigMap | wire → **`monitoring-config` App (done)** |
| `security/ci-cd/forgejo-oauth-setup-job.yaml` | 1 | Job+SA (PostSync) | wire → `cicd-config` App |
| CNPG operator + `forgejo-db` Cluster | — | CNPG install + Cluster | **Phase-0 manual bootstrap** (`bootstrap/phase2-cnpg`, `bootstrap/phase3-forgejo`), never an App — making it an App re-introduces the circle |
Root cause (monitoring): no App used `path: k8s/platform/monitoring` — only crds + valueFiles; the aggregator kustomization chain was dead. Now fixed by one `monitoring-config` App (one kustomization, **no `namespace:` transformer** — PrometheusRules live in per-app namespaces; `ServerSideApply=true` for large dashboard JSON).
### Stale / dead — delete, do NOT wire
- `applications/sqs/argocd/*` — **second competing app-of-apps** (repo `rock/kafaka-management-service.git`, project `kmsvc`).
- `applications/sqs/{queues,environments,helmfile.yaml.gotmpl,kustomization.yaml}` — helmfile-era.
- **`applications/temporal/elasticsearch.yaml`** — superseded (see Temporal datastore below).
- `applications/temporal/queues/`, root `applications/kustomization.yaml` — example / legacy overlay.
- `security/iam/authentik-migrations-job.yaml` — dead/broken (server self-migrates).
- Dead aggregator kustomizations (`infrastructure/`, `platform/`, `security/`, `ci-cd/`), `logging/minio-values.yaml`, `infrastructure/minio/test/`, scripts / `.env.example`.
**Experimental — leave unmanaged** (per `60-applications.yaml`): `applications/{dev-tools,forge,llm,shadowsocks}`.
### 🔴 Temporal datastore = PostgreSQL (hard constraint)
Temporal runs on the shared CNPG **PostgreSQL** (`ddb-cluster`), for BOTH the main store and **visibility**. **NOT Cassandra** (the Helm chart's default) and **NOT Elasticsearch.** `temporal-values.yaml` sets `elasticsearch.enabled: false` and points visibility at Postgres (`temporal-visibility-database`). Therefore `applications/temporal/elasticsearch.yaml` is dead → delete. When touching Temporal values, verify persistence stays on the pinned chart's SQL schema (chart schema drift silently reverts to Cassandra — see CLAUDE.md).
### Reconcile actions (tree → map)
1. **Restore** live-consumed paths currently staged-deleted: `infrastructure/{longhorn,minio}`, `security/iam`, `security/ci-cd/charts/forgejo-runner`, `platform/{logging,monitoring}` values + crds. Without them their Apps have no source.
2. **Add Apps:** `monitoring-config` (done), `cicd-config` (oauth job).
3. **Delete** the stale/dead list above.
4. **CNPG** (operator + `forgejo-db` Cluster) stays **Phase-0 manual bootstrap** — never an App (circular).
⚠️ The current staged deletion is **over-broad** — it wiped live GitOps sources (longhorn, minio, iam, monitoring/logging values). Committing as-is breaks those Apps' sync. Curate: restore live, delete only confirmed orphans.
ApplicationSet git-generator considered, rejected: the hand-tuned per-App settings (multi-source helm+values, `managedNamespaceMetadata`, `ServerSideApply`, waves) don't fit one template.
---
## Consequences
**Positive:** Forgejo/Longhorn/CNPG fully declarative; Phase-0 shrinks to "CNI + ArgoCD + root-app"; repeatable rebuild seeded from always-current GitHub (free off-site DR); CI shrinks to build/push/sign; digest-pinned auditable promotions; no privileged DinD; CI survives cp-1 loss; auto-rollback on SLO breach; only signed images admit; monitoring finally reconciled.
**Costs:** GitHub egress + deploy key needed at Phase 0; one cutover flip remains; two read creds (deploy key + SOPS PAT); three new controllers (Image Updater, Rollouts, Kyverno) + `Deployment`→`Rollout` migration; one-time registry-hostname normalization.
---
## Migration phases (one PR each, impact-over-risk)
0. **Consolidate bootstrap + coverage.** Single-source bootstrap; convert phase13 to ArgoCD Apps (seed repo); Forgejo pull-mirror + SOPS PAT; script = Phase-0 + cutover only. Curate the staged deletion (Part C reconcile): restore live paths, add `monitoring-config`(done)+`cicd-config`, delete stale/dead + `data/cluster`. CNPG (operator + `forgejo-db`) stays Phase-0 manual bootstrap.
1. **Kill `sed`.** `argocd-image-updater` (~w4); annotate `image-list` + git write-back; drop ci.yml step 3.
2. **Normalize registry.** In-cluster pulls via `forgejo-gitea-http.cicd.svc.cluster.local:3000`; retire `forgejo.forge.*`; pin digests.
3. **Rootless BuildKit.** Drop `az-a` nodeSelector; `--cache-to type=registry`.
4. **Argo Rollouts.** Controller (~w4); `api`/app tier → `Rollout` + canary `AnalysisTemplate`.
5. **Supply chain.** cosign (Forgejo OIDC) + Kyverno `verifyImages`.
6. **Consolidate projects.** One `homelab` project; monorepo-only vs single `deploy` repo.
7. **Synthetic E2E.** Wire `tests/e2e` suite as Rollouts/PostSync gate + `CronJob`; Portainer + Authentik + OAuth journeys.
---
## Open questions
1. ArgoCD install — `helm install` (leaning) vs Talos `extraManifests`.
2. Post-cutover primary — push GitHub (mirror pulls) vs push Forgejo (push-mirror out). Pull-from-GitHub keeps GitHub authoritative.
3. Cutover — scripted `kubectl patch` of root repoURL vs PostSync hook gated on Forgejo health.
---
## References
- `CLAUDE.md` — ArgoCD gotchas (SkipDryRunOnMissingResource, wait-for-Ready, repoURL/ingress hairpin, Helm schema drift → Temporal/Cassandra)
- `k8s/argocd/{root,apps,projects}/` · `k8s/bootstrap/` (to be superseded)
- `k8s/security/ci-cd/{example-workflows/ci.yml,charts/forgejo-runner,deploy-scaffold}`
- `tests/e2e/` (Playwright WebKit suite) · `k8s/platform/monitoring/` (monitoring-config)
-77
View File
@@ -1,77 +0,0 @@
#!/bin/bash
echo "🔧 Fixing Forgejo issues..."
echo ""
# Issue 1: Multi-Attach - old pod still holding the volume
echo "==> Issue 1: Cleaning up old Forgejo deployment"
echo "Current deployments:"
kubectl get deployment -n cicd | grep forgejo
echo ""
OLD_DEPLOYMENT=$(kubectl get deployment -n cicd -o name | grep -E "forgejo-[0-9]" | grep -v gitea)
if [ -n "$OLD_DEPLOYMENT" ]; then
echo "Found old deployment: $OLD_DEPLOYMENT"
kubectl delete $OLD_DEPLOYMENT -n cicd --wait=true
echo " ✓ Old deployment deleted"
else
echo " No old deployment found, checking for orphaned pods..."
kubectl get pods -n cicd -l app.kubernetes.io/name=gitea -o name | while read pod; do
POD_NAME=$(echo $pod | cut -d/ -f2)
if [[ ! "$POD_NAME" =~ "forgejo-gitea" ]]; then
echo " Deleting orphaned pod: $POD_NAME"
kubectl delete pod -n cicd $POD_NAME --force --grace-period=0
fi
done
fi
# Issue 2: Missing homelab-ca ConfigMap
echo ""
echo "==> Issue 2: Checking homelab-ca ConfigMap"
if kubectl get configmap homelab-ca -n cicd &>/dev/null; then
echo " ✓ homelab-ca already exists"
else
echo " ⚠️ homelab-ca not found in cicd namespace"
echo " Checking if it exists elsewhere..."
# Check common namespaces
for ns in default kube-system cert-manager; do
if kubectl get configmap homelab-ca -n $ns &>/dev/null; then
echo " Found in namespace: $ns"
echo " Copying to cicd namespace..."
kubectl get configmap homelab-ca -n $ns -o yaml | \
sed 's/namespace: '$ns'/namespace: cicd/' | \
kubectl apply -f -
echo " ✓ Copied homelab-ca to cicd"
break
fi
done
# If still not found, check if we need to create it
if ! kubectl get configmap homelab-ca -n cicd &>/dev/null; then
echo " Creating empty homelab-ca ConfigMap (you may need to populate it)..."
kubectl create configmap homelab-ca -n cicd --from-literal=ca.crt=""
echo " ⚠️ Created empty ConfigMap - update with actual CA if needed"
fi
fi
echo ""
echo "==> Waiting for volume detach (30s)..."
sleep 30
echo ""
echo "==> Current Forgejo pod status:"
kubectl get pods -n cicd -l app.kubernetes.io/name=gitea
echo ""
echo "==> If still in Init or Pending, describe one pod:"
POD=$(kubectl get pods -n cicd -l app.kubernetes.io/name=gitea --no-headers | head -1 | awk '{print $1}')
if [ -n "$POD" ]; then
kubectl describe pod -n cicd $POD | grep -A 10 "Events:" | head -15
fi
echo ""
echo "✅ Fixes applied!"
echo ""
echo "Next: Monitor pod startup"
echo " kubectl get pods -n cicd -w"
-6
View File
@@ -1,6 +0,0 @@
apiVersion: v2
name: claude-terminal
description: Persistent Claude CLI terminal running in tmux with web access via gotty
type: application
version: 1.0.0
appVersion: "1.0"
-31
View File
@@ -1,31 +0,0 @@
FROM --platform=linux/amd64 ubuntu:24.04
RUN apt-get update && apt-get install -y \
tmux \
curl \
git \
build-essential \
nodejs \
npm \
bash \
&& rm -rf /var/lib/apt/lists/*
# Install gotty (web terminal access)
RUN curl -sL https://github.com/sorenisanerd/gotty/releases/download/v1.5.0/gotty_linux_amd64.tar.gz | \
tar xz -C /usr/local/bin && chmod +x /usr/local/bin/gotty
# Install Claude CLI
RUN npm install -g claude-code-cli 2>&1 || echo "Note: Claude CLI will be available after NPM package is published"
WORKDIR /root
# Create persistent storage dir
RUN mkdir -p /root/.claude /root/.config /root/.cache
# Entrypoint: start tmux session and gotty
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 8080
ENTRYPOINT ["/entrypoint.sh"]
-49
View File
@@ -1,49 +0,0 @@
# Claude Terminal — Persistent Remote Dev Environment
Runs Claude CLI in a persistent tmux session with web-based terminal access via gotty.
## Building the Image
Build for `linux/amd64`:
```bash
cd homelab
docker buildx build --platform linux/amd64 \
-t forgejo.riotpiao.com/rock/claude-terminal:latest \
-f k8s/dev-tools/Dockerfile \
k8s/dev-tools
# Log in to Forgejo registry
docker login forgejo.riotpiao.com \
--username ci-bot \
--password "$(talos get cluster/iam/agents/ci-bot --key token)"
# Push
docker push forgejo.riotpiao.com/rock/claude-terminal:latest
```
Or use the provided build script:
```bash
./k8s/dev-tools/build.sh
```
## Deployment
Update `values.yaml` if needed, then deploy via helmfile:
```bash
helmfile apply -l name=claude-terminal
```
Access the terminal at: **https://claude.riotpiao.com**
## Persistent Storage
- All Claude configuration stored in `/root/.claude` (persistent PVC, 10Gi Longhorn)
- Survives pod restarts and node reboots
- Accessible immediately after reconnecting
## SSH Access (Optional)
To add SSH access, extend the Dockerfile to include openssh-server and mount the PVC as home directory.
-24
View File
@@ -1,24 +0,0 @@
#!/bin/bash
set -euo pipefail
REGISTRY="forgejo.riotpiao.com"
IMAGE_NAME="rock/claude-terminal"
TAG="latest"
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
echo "🔨 Building Claude Terminal image for linux/amd64..."
docker buildx build --platform linux/amd64 \
-t "${FULL_IMAGE}" \
-f Dockerfile \
. || { echo "❌ Build failed"; exit 1; }
echo "🔓 Logging in to Forgejo registry..."
REGISTRY_TOKEN=$(talos get cluster/iam/agents/ci-bot --key token)
echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" \
--username ci-bot \
--password-stdin || { echo "❌ Login failed"; exit 1; }
echo "📤 Pushing image to registry..."
docker push "${FULL_IMAGE}" || { echo "❌ Push failed"; exit 1; }
echo "✅ Successfully pushed ${FULL_IMAGE}"
-13
View File
@@ -1,13 +0,0 @@
#!/bin/bash
set -e
# Start tmux server in background
tmux new-session -d -s claude -c /root "bash"
# Give tmux a moment to stabilize
sleep 1
# Start gotty serving the tmux session
# -w: allow write (make terminal interactive)
# -p 8080: listen on port 8080
exec gotty -p 8080 -w tmux attach-session -t claude
@@ -1,5 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev-tools
resources: []
# Helm chart deployed via ArgoCD Helm source
@@ -1,49 +0,0 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "claude-terminal.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "claude-terminal.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "claude-terminal.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "claude-terminal.labels" -}}
helm.sh/chart: {{ include "claude-terminal.chart" . }}
{{ include "claude-terminal.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "claude-terminal.selectorLabels" -}}
app.kubernetes.io/name: {{ include "claude-terminal.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
@@ -1,57 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "claude-terminal.fullname" . }}
labels:
{{- include "claude-terminal.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "claude-terminal.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "claude-terminal.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: claude-terminal
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 8080
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 5
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: claude-storage
mountPath: {{ .Values.persistence.mountPath }}
volumes:
- name: claude-storage
persistentVolumeClaim:
claimName: {{ include "claude-terminal.fullname" . }}-pvc
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
@@ -1,41 +0,0 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "claude-terminal.fullname" . }}
labels:
{{- include "claude-terminal.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "claude-terminal.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}
@@ -1,15 +0,0 @@
{{- if .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "claude-terminal.fullname" . }}-pvc
labels:
{{- include "claude-terminal.labels" . | nindent 4 }}
spec:
accessModes:
- ReadWriteOnce
storageClassName: {{ .Values.persistence.storageClass }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- end }}
@@ -1,15 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "claude-terminal.fullname" . }}
labels:
{{- include "claude-terminal.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "claude-terminal.selectorLabels" . | nindent 4 }}
-45
View File
@@ -1,45 +0,0 @@
replicaCount: 1
image:
repository: localhost:5000/claude-terminal
pullPolicy: IfNotPresent
tag: latest
service:
type: ClusterIP
port: 8080
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: homelab-ca
hosts:
- host: claude.riotpiao.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: claude-terminal-tls
hosts:
- claude.riotpiao.com
persistence:
enabled: true
storageClass: longhorn
size: 10Gi
mountPath: /root/.claude
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
nodeSelector: {}
tolerations: []
affinity: {}
@@ -1,5 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: forge
resources:
- pki/
@@ -1,5 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: []
# PKI configuration, not K8s manifests
@@ -1,95 +0,0 @@
# k8s/forge/runner-gc-cronjob.yaml
# Garbage-collects the forgejo-runner's DinD layer cache (runner-dind PVC,
# 30Gi). Every CI build/pull only adds images and build-cache layers — there
# is no automatic pruning, so without this the PVC fills up and breaks builds.
#
# Runs `docker image prune` / `docker builder prune` inside the live dind
# container via `kubectl exec`, rather than a sidecar in the runner pod itself,
# so it can run on its own schedule independent of runner restarts.
apiVersion: v1
kind: ServiceAccount
metadata:
name: runner-gc
namespace: cicd
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: runner-gc
namespace: cicd
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: runner-gc
namespace: cicd
subjects:
- kind: ServiceAccount
name: runner-gc
namespace: cicd
roleRef:
kind: Role
name: runner-gc
apiGroup: rbac.authorization.k8s.io
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: forgejo-runner-image-gc
namespace: cicd
spec:
schedule: "0 3 * * *" # daily 03:00
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 600
template:
spec:
serviceAccountName: runner-gc
restartPolicy: Never
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
containers:
- name: gc
image: alpine/k8s:1.31.0
command:
- sh
- -c
- |
set -e
POD=$(kubectl -n cicd get pod -l app=forgejo-runner -o jsonpath='{.items[0].metadata.name}')
if [ -z "$POD" ]; then
echo "no forgejo-runner pod found, skipping"
exit 0
fi
echo "before:"
kubectl -n cicd exec "$POD" -c dind -- df -h /var/lib/docker
echo "pruning images unused for >72h on $POD"
kubectl -n cicd exec "$POD" -c dind -- docker image prune -af --filter "until=72h"
echo "pruning build cache unused for >72h on $POD"
kubectl -n cicd exec "$POD" -c dind -- docker builder prune -af --filter "until=72h"
echo "after:"
kubectl -n cicd exec "$POD" -c dind -- df -h /var/lib/docker
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 250m
memory: 128Mi
-438
View File
@@ -1,438 +0,0 @@
# k8s/forge/runner.yaml
# Forgejo Actions runner with Docker-in-Docker (DinD) sidecar.
# Phase 3.2 of talos_version_control.html.
#
# Pod layout (two containers, one pod):
# register initContainer — registers with Forgejo once; skips if .runner exists
# runner main container — long-running daemon that polls Forgejo for jobs
# dind sidecar — Docker daemon the runner talks to via mTLS on tcp://localhost:2376
#
# Prerequisites (Phase 3.1):
# TOKEN=$(kubectl -n cicd exec deploy/forgejo-gitea -- \
# gitea actions generate-runner-token 2>/dev/null | tr -d '\r\n')
# kubectl -n cicd create secret generic runner-token --from-literal=token="$TOKEN"
# # CA must come from cert-manager's homelab-ca-secret (the org-wide CA that
# # signs the live ingress cert), NOT k8s/forge/pki/ca.crt — that file is a
# # stale CA from before the "unified certificate" migration.
# kubectl get secret homelab-ca-secret -n cert-manager -o jsonpath='{.data.tls\.crt}' \
# | base64 -d | kubectl -n cicd create secret generic homelab-ca --from-file=ca.crt=/dev/stdin
#
# CA trust for ephemeral job containers (Phase 3.3):
# The homelab-ca secret above only reaches the runner/register/dind containers.
# It does NOT reach the per-job containers DinD spawns (e.g. golangci-lint,
# node:22-bookworm) — those are fresh sibling containers with their own trust
# store. Without this, any git/curl/apk call to forgejo.riotpiao.com
# from inside a job fails with "SSL certificate problem: unable to get local
# issuer certificate". Build a merged bundle (public roots + homelab CA) and
# bind-mount it over /etc/ssl/certs/ca-certificates.crt in every job
# container via forgejo-runner's config.yaml container.options.
#
# IMPORTANT: source the CA from the live cluster secret, NOT from
# k8s/forge/pki/ca.crt — that repo file is a stale CA from before the
# "unified certificate" migration (different key, fails verification
# against the cert actually served by forgejo.riotpiao.com). The
# org-wide CA that signs the live ingress cert lives in
# cert-manager/homelab-ca-secret, and cicd/homelab-ca above is already
# synced from it.
# docker run --rm docker:27-dind cat /etc/ssl/certs/ca-certificates.crt > /tmp/ca-bundle.crt
# kubectl -n cicd get secret homelab-ca -o jsonpath='{.data.ca\.crt}' | base64 -d >> /tmp/ca-bundle.crt
# kubectl -n cicd create secret generic ca-bundle --from-file=ca-certificates.crt=/tmp/ca-bundle.crt
# Re-run this whenever the homelab CA rotates (see talos-forge-trust.yaml).
#
# Apply:
# kubectl apply -f k8s/forge/runner.yaml
# kubectl -n cicd rollout status deploy/forgejo-runner
# kubectl -n cicd logs deploy/forgejo-runner -c runner -f
# # expect: "runner: daemon started" / "connected to Forgejo"
# ── PVCs ──────────────────────────────────────────────────────────────────────
# runner-reg — persists the .runner registration file so the runner doesn't
# re-register on every pod restart (token is one-use-per-registration)
# runner-dind — persists the Docker layer cache across pod restarts; keeps
# rebuilds fast — images don't need to be re-pulled every time
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: runner-reg
namespace: cicd
spec:
accessModes: [ReadWriteOnce]
storageClassName: longhorn
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: runner-dind
namespace: cicd
spec:
accessModes: [ReadWriteOnce]
storageClassName: longhorn
resources:
requests:
storage: 30Gi
---
# ── DinD TLS certs, issued by the homelab's unified CA ───────────────────────
# DinD's own entrypoint (dockerd-entrypoint.sh) self-generates a throwaway CA
# + server/client cert pair on every container start if none is supplied. Its
# server cert's SAN list only ever covers "docker", the pod hostname, and
# "localhost" - so anything reaching it via a stable Service DNS name (added
# below for story-crater-backend's release.yaml to build/push images) fails
# TLS hostname verification, even though the handshake itself succeeds.
#
# Fix: supply our own server+client cert pair, both issued by the same
# ClusterIssuer (homelab-ca) that already signs the live ingress cert, so
# they share one trust root. dockerd-entrypoint.sh skips its own generation
# step entirely once it finds $DOCKER_TLS_CERTDIR/server/{ca,cert,key}.pem
# already present and no CA private key alongside them (confirmed by reading
# the script directly: `kubectl exec -n cicd <pod> -c dind -- cat
# /usr/local/bin/dockerd-entrypoint.sh`) - exactly the "bring your own CA"
# path it's designed for.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: forgejo-runner-dind-server
namespace: cicd
spec:
secretName: forgejo-runner-dind-server-tls
issuerRef:
name: homelab-ca
kind: ClusterIssuer
commonName: docker:dind server
dnsNames:
- forgejo-runner-dind.cicd.svc.cluster.local
- forgejo-runner-dind.cicd.svc
- forgejo-runner-dind
- docker
- localhost
usages:
- server auth
- digital signature
- key encipherment
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: forgejo-runner-dind-client
namespace: cicd
spec:
secretName: forgejo-runner-dind-client-tls
issuerRef:
name: homelab-ca
kind: ClusterIssuer
commonName: docker:dind client
usages:
- client auth
- digital signature
- key encipherment
---
# Stable address for the dind sidecar's docker API (2376, mTLS) - lets
# CI workflows (e.g. story-crater-backend's release.yaml) build/push images
# by reaching this runner's own already-working outer dind directly, instead
# of the per-job `services:` sidecar pattern (confirmed broken: act-runner
# never registers a DNS alias for service containers - job container's
# /etc/hosts has no entry for it, `docker info` fails with a DNS lookup
# error, not a TLS/connection error).
apiVersion: v1
kind: Service
metadata:
name: forgejo-runner-dind
namespace: cicd
spec:
selector:
app: forgejo-runner
ports:
- port: 2376
targetPort: 2376
---
# ── Runner config ─────────────────────────────────────────────────────────────
# container.options is appended to every `docker run` DinD issues for a job
# container, so this is what actually gets the merged CA bundle (ca-bundle
# secret, see header comment) trusted inside golangci-lint, node, etc. - and
# now also what propagates the homelab-CA-signed client cert (above) into
# job containers that need to talk back to dind themselves (e.g. building
# and pushing images).
# Source paths resolve against the dind container's filesystem (it's the
# daemon creating these containers), so both are mounted into dind below.
apiVersion: v1
kind: ConfigMap
metadata:
name: forgejo-runner-config
namespace: cicd
data:
config.yaml: |
container:
options: -v /etc/forgejo-ca/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro -v /docker-certs/client:/docker-certs/client:ro
# Without this, sanitizeConfig() in forgejo/act silently drops any bind
# mount whose source isn't allowlisted here — including ones injected
# via container.options above, not just workflow-declared volumes.
valid_volumes:
- /etc/forgejo-ca/ca-certificates.crt
- /docker-certs/client
---
# ── Deployment ────────────────────────────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: forgejo-runner
namespace: cicd
spec:
replicas: 1
# RWO PVCs mean only one pod can mount them at a time.
# Recreate ensures the old pod fully terminates before the new one starts.
strategy:
type: Recreate
selector:
matchLabels:
app: forgejo-runner
template:
metadata:
labels:
app: forgejo-runner
spec:
# runner/register containers run as uid 1000 (image default); fsGroup
# makes kubelet chown+chmod the Longhorn PVC's group to 1000 with
# write access, otherwise writes to /data (.runner config) fail with
# "permission denied" since the volume is root:root 755 by default.
securityContext:
fsGroup: 1000
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
initContainers:
# Registers the runner with Forgejo exactly once.
# test -f /data/.runner makes it idempotent — re-registration would
# consume the one-time token and break the runner.
- name: register
image: code.forgejo.org/forgejo/runner:6
command: ["sh", "-c"]
args:
- |
test -f /data/.runner && echo "already registered, skipping" && exit 0
forgejo-runner register --no-interactive \
--instance https://forgejo.riotpiao.com \
--token "$RUNNER_TOKEN" \
--name talos-runner \
--labels "docker:docker://node:22-bookworm"
env:
- name: RUNNER_TOKEN
valueFrom:
secretKeyRef:
name: runner-token
key: token
volumeMounts:
- name: runner-data
mountPath: /data
# CA cert so the register call can verify Forgejo's TLS cert
- name: homelab-ca
mountPath: /etc/ssl/certs/homelab-ca.pem
subPath: ca.crt
workingDir: /data
containers:
# ── Runner daemon ────────────────────────────────────────────────────
# Polls Forgejo for pending jobs and executes them inside DinD.
# The `until docker info` loop waits for the DinD sidecar to finish
# its TLS setup before starting the daemon — without this the runner
# starts before Docker is ready and immediately errors out.
- name: runner
image: code.forgejo.org/forgejo/runner:6
command: ["sh", "-c"]
args:
- |
until nc -z localhost 2376 >/dev/null 2>&1; do
echo "waiting for docker daemon..."; sleep 2
done
forgejo-runner daemon --config /data/config.yaml
workingDir: /data
env:
# Connect to the DinD sidecar via mTLS on localhost
- name: DOCKER_HOST
value: tcp://localhost:2376
- name: DOCKER_TLS_VERIFY
value: "1"
- name: DOCKER_CERT_PATH
value: /docker-certs/client
volumeMounts:
- name: runner-data
mountPath: /data
- name: docker-certs
mountPath: /docker-certs
- name: homelab-ca
mountPath: /etc/ssl/certs/homelab-ca.pem
subPath: ca.crt
# forgejo-runner's container.options, read from this file, is what
# propagates the CA bundle into per-job containers (see ca-bundle
# secret + dind mount below)
- name: runner-config
mountPath: /data/config.yaml
subPath: config.yaml
# Homelab-CA-signed client cert (overlays whatever's in the
# docker-certs emptyDir at this subpath) - matches the server
# cert dind now presents, see Certificates above.
- name: dind-client-tls
mountPath: /docker-certs/client
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "2"
memory: 4Gi
# ── DinD sidecar ──────────────────────────────────────────────────────
# Full Docker daemon running inside the pod.
# privileged: true is required for DinD — the cicd namespace is labelled
# pod-security.kubernetes.io/enforce=privileged to allow this.
# DOCKER_TLS_CERTDIR causes DinD to generate mTLS certs in /docker-certs
# on startup; the runner reads the client certs from /docker-certs/client.
# runner-dind PVC mounts /var/lib/docker so the layer cache persists
# across pod restarts.
- name: dind
image: docker:27-dind
securityContext:
privileged: true
env:
- name: DOCKER_TLS_CERTDIR
value: /docker-certs
volumeMounts:
- name: docker-certs
mountPath: /docker-certs
- name: dind-storage
mountPath: /var/lib/docker
# Trust the homelab CA so DinD can pull from Forgejo's OCI registry
- name: homelab-ca
mountPath: /etc/ssl/certs/homelab-ca.pem
subPath: ca.crt
# Merged CA bundle (public roots + homelab CA), bind-mounted from
# here into every job container by container.options above —
# this path is resolved against dind's filesystem since dind is
# the daemon actually creating those containers.
- name: ca-bundle
mountPath: /etc/forgejo-ca/ca-certificates.crt
subPath: ca-certificates.crt
# Homelab-CA-signed server+client certs (see Certificates above),
# overlaying the matching subpaths of the docker-certs emptyDir.
# dockerd-entrypoint.sh detects these and skips its own
# self-signed generation entirely (no CA private key is supplied
# alongside them, so it can't regenerate even if it wanted to).
- name: dind-server-tls
mountPath: /docker-certs/server
- name: dind-client-tls
mountPath: /docker-certs/client
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "2"
memory: 4Gi
volumes:
- name: runner-data
persistentVolumeClaim:
claimName: runner-reg
- name: dind-storage
persistentVolumeClaim:
claimName: runner-dind
# emptyDir parent mount for /docker-certs - the server/ and client/
# subpaths are now overlaid by the homelab-CA-signed dind-server-tls/
# dind-client-tls secret mounts below (dockerd-entrypoint.sh no
# longer self-generates once it finds those present). This emptyDir
# just needs to exist as the parent directory; nothing writes
# directly to it anymore.
- name: docker-certs
emptyDir: {}
- name: homelab-ca
secret:
secretName: homelab-ca
- name: ca-bundle
secret:
secretName: ca-bundle
- name: runner-config
configMap:
name: forgejo-runner-config
# cert-manager issues these as tls.crt/tls.key/ca.crt - remapped to
# the ca.pem/cert.pem/key.pem filenames dockerd-entrypoint.sh expects
# under $DOCKER_TLS_CERTDIR/{server,client}/.
- name: dind-server-tls
secret:
secretName: forgejo-runner-dind-server-tls
items:
- key: ca.crt
path: ca.pem
- key: tls.crt
path: cert.pem
- key: tls.key
path: key.pem
- name: dind-client-tls
secret:
secretName: forgejo-runner-dind-client-tls
items:
- key: ca.crt
path: ca.pem
- key: tls.crt
path: cert.pem
- key: tls.key
path: key.pem
---
# ── NetworkPolicy ─────────────────────────────────────────────────────────────
# Restrict runner egress: it may only reach Forgejo (cicd ns), CoreDNS, and
# the public internet for action dependencies and base images.
# LAN (192.168.1.0/24) and the pod network (10.244.0.0/16) are blocked to
# prevent a compromised CI job from pivoting into the cluster or LAN.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: runner-egress
namespace: cicd
spec:
podSelector:
matchLabels:
app: forgejo-runner
policyTypes: [Egress]
egress:
# Forgejo (same namespace — git push, OCI registry push/pull)
- to:
- podSelector: {}
# ingress-nginx (the runner talks to Forgejo via its public hostname,
# https://forgejo.riotpiao.com, which resolves to the ingress
# controller's ClusterIP — a different namespace on the pod network)
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- protocol: TCP
port: 443
- protocol: TCP
port: 80
# CoreDNS (DNS resolution for action deps and Forgejo hostname)
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Public internet for action dependencies and base images
# LAN and pod network are explicitly excluded
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 192.168.1.0/24
- 10.244.0.0/16
-19
View File
@@ -1,19 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
metadata:
name: applications
# Layer 5: Applications — user services, workloads
# Dependencies: all previous layers (bootstrap, platform, security)
# Order: Applied fifth
resources:
- temporal/
- sqs/
- llm/
- portainer/
- forge/
- shadowsocks/
- dev-tools/
- duckdns/
- cloudflared/
-299
View File
@@ -1,299 +0,0 @@
# Ollama LLM Inference Service
CPU-only LLM inference server on talos-cp-1. Single model hot-loaded (DeepSeek-R1:70b), 42GB, 70Gi memory limit.
## Quick Start
### Access via port-forward
```bash
kubectl -n llm port-forward svc/ollama 11434:11434
curl http://localhost:11434/api/tags
```
### Debug pod (in-cluster)
```bash
kubectl run debug --rm -it -n llm --image=curlimages/curl \
--labels="app.kubernetes.io/role=llm-debug" \
--serviceaccount=llm-worker -- sh
# Inside pod
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -H "Authorization: Bearer $TOKEN" \
http://ollama.llm.svc.cluster.local:11434/api/tags
```
## Architecture
| Component | Value |
|-----------|-------|
| Service | ClusterIP `ollama.llm.svc.cluster.local:11434` |
| Namespace | `llm` |
| Node | talos-cp-1 (pinned via nodeAffinity) |
| Memory request | 50Gi |
| Memory limit | 70Gi |
| Storage | 115Gi PVC (Longhorn) |
| Model | `deepseek-r1:70b` (~42GB) |
| Max loaded | 1 model |
| Parallelism | 1 request at a time |
## API Endpoints
### List models
```bash
curl http://ollama.llm.svc.cluster.local:11434/api/tags
```
Response:
```json
{
"models": [
{"name": "deepseek-r1:70b", "size": 42000000000, ...}
]
}
```
### Generate (non-streaming)
```bash
curl -X POST http://ollama.llm.svc.cluster.local:11434/api/generate \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-r1:70b",
"prompt": "Why is the sky blue?",
"stream": false
}'
```
### Pull model
```bash
curl -X POST http://ollama.llm.svc.cluster.local:11434/api/pull \
-H "Content-Type: application/json" \
-d '{"name": "deepseek-r1:70b", "stream": false}'
```
## Operations
### Check pod status
```bash
kubectl -n llm get pod -l app.kubernetes.io/name=ollama
kubectl -n llm describe pod -l app.kubernetes.io/name=ollama
```
### View logs
```bash
kubectl -n llm logs deployment/ollama -f
```
### Monitor download progress (bootstrap)
```bash
kubectl -n llm logs -f job/bootstrap-models -c model-download
```
### Restart deployment
```bash
kubectl -n llm rollout restart deployment/ollama
```
## Storage
- **PVC:** `ollama-models-cache`, 115Gi, Longhorn StorageClass
- **Mount:** `/root/.ollama/models` (Ollama model cache)
- **Lifecycle:** RWO (Read-Write-Once), tied to talos-cp-1
### Resize PVC
⚠️ PVCs can only expand, not shrink. Edit values.yaml and redeploy:
```yaml
pvc:
size: 120Gi # increase only
```
```bash
vsource .env && helmfile -f helmfile.yaml.gotmpl -l name=ollama apply
```
## Networking
### NetworkPolicy
- Default-deny ingress on Ollama pods
- Allow from pods labeled `app.kubernetes.io/name: llm-worker` (port 11434)
- Allow from pods labeled `app.kubernetes.io/role: llm-debug` (port 11434)
View policy:
```bash
kubectl -n llm get networkpolicy ollama
```
Test access from external pod (should fail):
```bash
kubectl run test --rm -it --image=curlimages/curl -- \
curl http://ollama.llm.svc.cluster.local:11434/
# Connection timeout (correct)
```
Test access from debug pod (should succeed):
```bash
kubectl -n llm logs job/bootstrap-models # verify bootstrap completed
# Then run debug pod as shown above
```
## Configuration
### Helm values (`k8s/llm/charts/ollama/values.yaml`)
```yaml
resources:
requests:
cpu: 8
memory: 50Gi
limits:
cpu: 16
memory: 70Gi
env:
OLLAMA_MAX_LOADED_MODELS: "1"
OLLAMA_NUM_PARALLEL: "1"
OLLAMA_MAX_QUEUE: "32"
OLLAMA_KEEP_ALIVE: "-1"
OLLAMA_HOST: "0.0.0.0:11434"
preloadJob:
enabled: true
hotModels:
- deepseek-r1:70b
```
### Environment variables
| Variable | Value | Purpose |
|----------|-------|---------|
| `OLLAMA_MODELS` | `/root/.ollama/models` | Model cache dir |
| `OLLAMA_MAX_LOADED_MODELS` | `1` | Max concurrent models in RAM |
| `OLLAMA_NUM_PARALLEL` | `1` | Parallel request threads |
| `OLLAMA_MAX_QUEUE` | `32` | Request queue depth |
| `OLLAMA_KEEP_ALIVE` | `-1` | Keep model resident (never unload) |
| `OLLAMA_HOST` | `0.0.0.0:11434` | Bind address |
Tune `OLLAMA_NUM_PARALLEL` based on CPU cores. Current: 1 (conservative, CPU bottleneck).
## Model Management
### Current model
- **Name:** `deepseek-r1:70b`
- **Size:** ~42GB
- **Quantization:** Default Ollama quant
- **Status:** Downloaded during pod init via bootstrap job
### Change model
1. Edit `values.yaml`:
```yaml
preloadJob:
hotModels:
- deepseek-r1:32b # or any available model
```
2. Redeploy:
```bash
kubectl -n llm delete job bootstrap-models --ignore-not-found
vsource .env && helmfile -f helmfile.yaml.gotmpl -l name=ollama apply
```
3. Monitor:
```bash
kubectl -n llm logs -f job/bootstrap-models -c model-download
```
### Available models
Ollama registry: https://ollama.com/library
Examples:
- `deepseek-r1:70b` (reasoning, 42GB)
- `deepseek-r1:32b` (faster, 20GB)
- `llama3.1:70b` (general, 41GB)
- `mistral:large` (26GB)
## Troubleshooting
### Pod stuck in `ContainerCreating`
```bash
kubectl -n llm describe pod -l app.kubernetes.io/name=ollama
# Check Events section for PVC/image pull issues
```
### Bootstrap job failing
```bash
kubectl -n llm logs job/bootstrap-models -c model-download --tail=50
# Common: model not found in registry, disk full, network timeout
```
### Model pull timeout
```bash
# Increase pod timeout (edit deployment directly)
kubectl -n llm edit deployment ollama
# Change readinessProbe.initialDelaySeconds, livenessProbe.periodSeconds
```
### Out of memory
Model size exceeds limit. Reduce `memory.limits` or choose smaller model.
```bash
kubectl top pod -n llm # check actual usage
```
### Cannot connect from other pods
Verify NetworkPolicy:
```bash
kubectl -n llm get networkpolicy
kubectl -n llm describe networkpolicy ollama
# Add pod label: app.kubernetes.io/name: llm-worker or app.kubernetes.io/role: llm-debug
```
## Secrets
Ollama pod receives MinIO credentials via Secret `ollama-minio` (created by helmfile presync):
```bash
kubectl -n llm get secret ollama-minio -o jsonpath='{.data}' | jq
```
Keys: `endpoint`, `bucket`, `access_key`, `secret_key`
Used by bootstrap job to upload model blobs to MinIO (future: auto-backup).
## Metrics & Observability
### Prometheus scrape (if enabled)
ServiceMonitor: Not yet configured (see `k8s/monitoring/dashboards/services/`)
Metrics to add:
- `ollama_requests_total` (counter)
- `ollama_request_duration_seconds` (histogram)
- `ollama_loaded_models` (gauge)
### Logs
Pod logs via kubectl. No log aggregation to Loki yet.
```bash
kubectl -n llm logs deployment/ollama -f --timestamps
```
## Cleanup
### Delete Ollama completely
```bash
vsource .env && helmfile -f helmfile.yaml.gotmpl -l name=ollama destroy
# Keeps PVC (data safety). To delete: kubectl -n llm delete pvc ollama-models-cache
```
### Delete just the model cache (keep deployment)
```bash
kubectl -n llm delete pvc ollama-models-cache
# Recreate: kubectl -n llm patch deployment ollama -p '{"spec":{"template":{"metadata":{"annotations":{"restart":"now"}}}}}'
```
## See Also
- Helmfile: `helmfile.yaml.gotmpl` (llm release block)
- Chart: `k8s/llm/charts/ollama/`
- Namespace: `llm`
- Bootstrap: `k8s/llm/bootstrap-models-job.yaml` (manual preload fallback)
@@ -1,6 +0,0 @@
apiVersion: v2
name: ollama
description: CPU-only Ollama LLM server with MinIO model registry
type: application
version: 0.1.0
appVersion: "latest"
@@ -1,126 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ollama
namespace: llm
labels:
app.kubernetes.io/name: ollama
app.kubernetes.io/part-of: llm
spec:
replicas: {{ .Values.replicaCount }}
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: ollama
template:
metadata:
labels:
app.kubernetes.io/name: ollama
app.kubernetes.io/part-of: llm
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- {{ .Values.nodeAffinity.zone }}
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Equal
value: ""
effect: NoSchedule
initContainers:
- name: preload-model
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command:
- sh
- -c
- |
set -e
echo "Starting Ollama server for model preload..."
ollama serve &
OLLAMA_PID=$!
sleep 10
{{- range .Values.preloadJob.hotModels }}
echo "Preloading {{ . }}..."
if ollama ls | grep -q "{{ . }}"; then
echo "✓ {{ . }} already cached"
else
ollama pull {{ . }}
fi
{{- end }}
echo "Model preload complete"
kill $OLLAMA_PID || true
wait $OLLAMA_PID 2>/dev/null || true
volumeMounts:
- name: models-cache
mountPath: /root/.ollama/models
env:
- name: OLLAMA_HOST
value: "127.0.0.1:11434"
containers:
- name: ollama
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: 11434
name: http
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: "{{ $value }}"
{{- end }}
- name: OLLAMA_MODELS_MINIO_ENDPOINT
valueFrom:
secretKeyRef:
name: ollama-minio
key: endpoint
- name: OLLAMA_MODELS_MINIO_BUCKET
valueFrom:
secretKeyRef:
name: ollama-minio
key: bucket
- name: OLLAMA_MODELS_MINIO_ACCESS_KEY
valueFrom:
secretKeyRef:
name: ollama-minio
key: access_key
- name: OLLAMA_MODELS_MINIO_SECRET_KEY
valueFrom:
secretKeyRef:
name: ollama-minio
key: secret_key
resources:
requests:
cpu: {{ .Values.resources.requests.cpu }}
memory: {{ .Values.resources.requests.memory }}
limits:
cpu: {{ .Values.resources.limits.cpu }}
memory: {{ .Values.resources.limits.memory }}
livenessProbe:
httpGet:
path: /
port: 11434
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 11434
initialDelaySeconds: 10
periodSeconds: 5
volumeMounts:
- name: models-cache
mountPath: /root/.ollama/models
volumes:
- name: models-cache
persistentVolumeClaim:
claimName: ollama-models-cache
@@ -1,28 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ollama-access
namespace: llm
labels:
app.kubernetes.io/name: ollama
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: ollama
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: llm-worker
ports:
- protocol: TCP
port: 11434
- from:
- podSelector:
matchLabels:
app.kubernetes.io/role: llm-debug
ports:
- protocol: TCP
port: 11434
@@ -1,92 +0,0 @@
{{- if .Values.preloadJob.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
name: ollama-preload
namespace: llm
labels:
app.kubernetes.io/name: ollama-preload
spec:
backoffLimit: 3
template:
spec:
serviceAccountName: default
restartPolicy: Never
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- az-a
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Equal
effect: NoSchedule
initContainers:
- name: model-cache-init
image: ollama/ollama:latest
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
set -e
echo "Starting Ollama server to cache models..."
ollama serve &
OLLAMA_PID=$!
sleep 10
echo "Caching hot-tier models..."
{{- range .Values.preloadJob.hotModels }}
echo "Checking if {{ . }} is cached..."
if ollama ls | grep -q "{{ . }}"; then
echo "✓ {{ . }} already cached, skipping"
else
echo "Pulling {{ . }}..."
ollama pull {{ . }}
fi
{{- end }}
echo "Model cache initialization complete"
kill $OLLAMA_PID || true
wait $OLLAMA_PID 2>/dev/null || true
volumeMounts:
- name: models
mountPath: /root/.ollama
env:
- name: OLLAMA_HOST
value: "127.0.0.1:11434"
containers:
- name: cache-populate
image: curlimages/curl:latest
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
set -e
echo "Waiting for Ollama pod to be ready..."
until curl -f http://ollama.llm.svc.cluster.local:11434/api/tags 2>/dev/null; do
echo "Ollama not ready, waiting..."
sleep 5
done
echo "Ollama is ready, populating local cache..."
{{- range .Values.preloadJob.hotModels }}
echo "Checking if {{ . }} is already cached..."
if curl -s http://ollama.llm.svc.cluster.local:11434/api/tags | grep -q "{{ . }}"; then
echo "✓ {{ . }} already cached, skipping"
else
echo "Caching {{ . }} locally..."
curl -X POST http://ollama.llm.svc.cluster.local:11434/api/pull \
-H "Content-Type: application/json" \
-d '{"name":"{{ . }}","stream":false}'
fi
{{- end }}
echo "Local cache population complete"
volumes:
- name: models
emptyDir: {}
{{- end }}
@@ -1,14 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ollama-models-cache
namespace: llm
labels:
app.kubernetes.io/name: ollama
spec:
accessModes:
- ReadWriteOnce
storageClassName: {{ .Values.pvc.storageClassName }}
resources:
requests:
storage: {{ .Values.pvc.size }}
@@ -1,16 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: ollama
namespace: llm
labels:
app.kubernetes.io/name: ollama
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: ollama
@@ -1,12 +0,0 @@
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: longhorn-llm
labels:
app.kubernetes.io/name: ollama
provisioner: driver.longhorn.io
parameters:
numberOfReplicas: "1"
staleReplicaTimeout: "2880"
reclaimPolicy: Retain
allowVolumeExpansion: true
@@ -1,40 +0,0 @@
replicaCount: 1
image:
repository: ollama/ollama
pullPolicy: IfNotPresent
tag: "latest"
service:
type: ClusterIP
port: 11434
resources:
requests:
cpu: 8
memory: 60Gi
limits:
cpu: 16
memory: 100Gi
pvc:
enabled: true
size: 115Gi
storageClassName: longhorn-llm
nodeAffinity:
zone: az-a
env:
OLLAMA_MODELS: /root/.ollama/models
OLLAMA_MAX_LOADED_MODELS: "2"
OLLAMA_NUM_PARALLEL: "2"
OLLAMA_MAX_QUEUE: "64"
OLLAMA_KEEP_ALIVE: "-1"
OLLAMA_HOST: "0.0.0.0:11434"
preloadJob:
enabled: false
hotModels:
- ornith:35b
- deepseek-r1:70b
-6
View File
@@ -1,6 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: llm
resources:
- scripts/
# Helm charts deployed via ArgoCD Helm source
@@ -1,5 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: []
# Shell scripts, not K8s manifests
@@ -1,71 +0,0 @@
#!/bin/bash
# setup-minio-bucket.sh
# Creates MinIO bucket and Kubernetes secrets for Ollama LLM server
# Runs as helmfile presync hook; all commands are idempotent
# Dependencies: kubectl, access to minio-az-a pod in storage namespace
# Environment: MINIO_ROOT_USER, MINIO_ROOT_PASSWORD (from Vault), AUTHENTIK_OLLAMA_CLIENT_ID, AUTHENTIK_OLLAMA_CLIENT_SECRET
set -e
echo "=== Step 1: Create and label llm namespace ==="
kubectl create namespace llm --dry-run=client -o yaml | kubectl apply -f -
kubectl label namespace llm \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/enforce-version=latest \
--overwrite
echo "✓ llm namespace created/labeled"
echo ""
echo "=== Step 2: Create MinIO bucket riotpiao-models ==="
# Configure mc host inside MinIO pod
kubectl -n storage exec deployment/minio-az-a -- \
mc config host add local http://localhost:9000 \
"${MINIO_ROOT_USER}" "${MINIO_ROOT_PASSWORD}"
echo "✓ mc host configured"
# Create bucket (idempotent)
kubectl -n storage exec deployment/minio-az-a -- \
mc mb --ignore-existing local/riotpiao-models
echo "✓ MinIO bucket riotpiao-models created (or already exists)"
# Enable versioning for model rollback safety
kubectl -n storage exec deployment/minio-az-a -- \
mc version enable local/riotpiao-models
echo "✓ Versioning enabled on riotpiao-models bucket"
echo ""
echo "=== Step 3: Create ollama-minio Secret (MinIO credentials) ==="
kubectl create secret generic ollama-minio -n llm \
--from-literal=endpoint="http://minio-az-a.storage:9000" \
--from-literal=bucket="riotpiao-models" \
--from-literal=access_key="${MINIO_ROOT_USER}" \
--from-literal=secret_key="${MINIO_ROOT_PASSWORD}" \
--dry-run=client -o yaml | kubectl apply -f -
echo "✓ Secret ollama-minio created/updated"
echo ""
echo "=== Step 4: Create ollama-oidc Secret (Authentik credentials) ==="
kubectl create secret generic ollama-oidc -n llm \
--from-literal=client_id="${AUTHENTIK_OLLAMA_CLIENT_ID}" \
--from-literal=client_secret="${AUTHENTIK_OLLAMA_CLIENT_SECRET}" \
--dry-run=client -o yaml | kubectl apply -f -
echo "✓ Secret ollama-oidc created/updated"
echo ""
echo "=== Verification ==="
echo ""
echo "Run these commands to verify:"
echo " kubectl -n llm get secret ollama-minio ollama-oidc"
echo " kubectl -n storage exec deployment/minio-az-a -- mc ls local/riotpiao-models"
echo ""
echo "Setup complete!"
-54
View File
@@ -1,54 +0,0 @@
#!/usr/bin/env bash
# portainer/bootstrap.sh
# Deploys Portainer CE into the dashboard namespace.
# No credentials needed — Portainer prompts you to create an admin account
# on first browser visit.
#
# Prerequisites:
# - kubectl configured (KUBECONFIG pointing to cluster-config/kubeconfig)
# - helm >= 3.x
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
KUBECONFIG="${KUBECONFIG:-${REPO_ROOT}/cluster-config/kubeconfig}"
export KUBECONFIG
# ── Namespace ─────────────────────────────────────────────────────────────────
echo "==> Creating dashboard namespace..."
kubectl create namespace dashboard --dry-run=client -o yaml | kubectl apply -f -
kubectl label namespace dashboard \
pod-security.kubernetes.io/enforce=privileged \
pod-security.kubernetes.io/enforce-version=latest \
--overwrite
# ── Helm repo ─────────────────────────────────────────────────────────────────
echo "==> Adding Portainer Helm repo..."
helm repo add portainer https://portainer.github.io/k8s/
helm repo update portainer
# ── Portainer ─────────────────────────────────────────────────────────────────
echo "==> Installing Portainer..."
helm upgrade --install portainer portainer/portainer \
--namespace dashboard \
--values "${SCRIPT_DIR}/portainer-values.yaml" \
--wait \
--timeout 5m
echo "==> Waiting for Portainer Deployment to be ready..."
kubectl rollout status deployment/portainer -n dashboard --timeout=120s
# ── Done ──────────────────────────────────────────────────────────────────────
echo ""
echo "==> Portainer is up."
echo ""
echo "Access Portainer UI:"
echo " make pf-portainer"
echo " http://localhost:9000"
echo ""
echo "First-time setup: Portainer will prompt you to create an admin account."
echo "Choose 'Manage the local Kubernetes environment' when asked."
echo ""
echo "Node failure resilience tip:"
echo " For faster PVC failover on hard node failure, enable in Longhorn UI → Settings:"
echo " nodeDownPodDeletionPolicy = delete-deployment-pod"
@@ -1,4 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: shadowsocks
resources: []
@@ -1,159 +0,0 @@
# k8s/shadowsocks/shadowsocks.yaml
# Personal Shadowsocks proxy (for Shadowrocket/other SS clients) — an
# alternative tunnel to the WireGuard setup in cluster-config/, useful when
# a network blocks/throttles WireGuard but not generic TLS-looking traffic.
#
# Everything that varies between deployments (port, password, method) lives
# in the shadowsocks-config Secret below — the Deployment/Service never
# hardcode a value, so re-pointing this at a new port or rotating the
# password is a Secret edit + rollout restart, no YAML edit.
#
# Prerequisites:
# talos put cluster/SHADOWSOCKS_PASSWORD SHADOWSOCKS_PASSWORD="$(openssl rand -base64 24)"
# talos put cluster/SHADOWSOCKS_PORT SHADOWSOCKS_PORT="8388"
#
# Apply:
# kubectl create namespace vpn --dry-run=client -o yaml | kubectl apply -f -
# kubectl -n vpn create secret generic shadowsocks-config \
# --from-literal=SERVER_PORT="$(talos get cluster/SHADOWSOCKS_PORT --key SHADOWSOCKS_PORT)" \
# --from-literal=PASSWORD="$(talos get cluster/SHADOWSOCKS_PASSWORD --key SHADOWSOCKS_PASSWORD)" \
# --from-literal=METHOD="aes-256-gcm" \
# --from-literal=TIMEOUT="300"
# kubectl apply -f k8s/shadowsocks/shadowsocks.yaml
#
# Rotate password (or change port) later:
# kubectl -n vpn delete secret shadowsocks-config && <recreate with new values>
# kubectl -n vpn rollout restart deploy/shadowsocks
#
# Client config: SERVER_PORT/METHOD/PASSWORD above feed directly into the
# Shadowrocket/SS client's server, method, and password fields. SERVER_ADDR
# for the client is the LB IP below (192.168.1.166), or your router's WAN
# address/DDNS hostname (riotpiao.duckdns.org) with port-forwarding to it —
# same pattern as the wg1 WireGuard peer in cluster-config/phone_config.conf.
apiVersion: v1
kind: Namespace
metadata:
name: vpn
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: shadowsocks
namespace: vpn
spec:
replicas: 1
selector:
matchLabels:
app: shadowsocks
template:
metadata:
labels:
app: shadowsocks
spec:
containers:
- name: shadowsocks
image: shadowsocks/shadowsocks-libev:latest
env:
- name: SERVER_ADDR
value: "0.0.0.0"
- name: SERVER_PORT
valueFrom:
secretKeyRef:
name: shadowsocks-config
key: SERVER_PORT
- name: PASSWORD
valueFrom:
secretKeyRef:
name: shadowsocks-config
key: PASSWORD
- name: METHOD
valueFrom:
secretKeyRef:
name: shadowsocks-config
key: METHOD
- name: TIMEOUT
valueFrom:
secretKeyRef:
name: shadowsocks-config
key: TIMEOUT
# containerPort is informational only (no portRange support for
# env-driven SERVER_PORT) — the Service below is what actually
# routes traffic, matched on the same Secret key via downward API
# isn't available for Service ports, so targetPort uses the literal
# port name instead; see Service ports comment.
ports:
- containerPort: 8388
protocol: TCP
- containerPort: 8388
protocol: UDP
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 500m
memory: 128Mi
---
# LoadBalancer via Cilium LB-IPAM (see k8s/cilium/lb-ipam-pool.yaml) — pinned
# to .166 so router port-forwarding and the DDNS hostname stay stable across
# pod/service recreates, same pattern forgejo uses at .165.
#
# NOTE: SERVER_PORT in the Secret must match port/targetPort/nodePort here.
# If you change the port, update both the Secret and this Service together.
apiVersion: v1
kind: Service
metadata:
name: shadowsocks
namespace: vpn
annotations:
io.cilium/lb-ipam-ips: "192.168.1.166"
spec:
type: LoadBalancer
selector:
app: shadowsocks
ports:
- name: tcp
protocol: TCP
port: 8388
targetPort: 8388
- name: udp
protocol: UDP
port: 8388
targetPort: 8388
---
# Restrict egress like the forgejo-runner pattern (k8s/forge/runner.yaml) —
# a proxy server is, by design, an open relay to the internet for whoever
# holds the password; LAN/pod-network egress is blocked so a compromised
# password can't be used to pivot into the cluster or LAN. CoreDNS is
# explicitly allowed — shadowsocks-libev resolves client-requested hostnames
# itself, so blanket-blocking the service subnet would break that.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: shadowsocks-egress
namespace: vpn
spec:
podSelector:
matchLabels:
app: shadowsocks
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 192.168.1.0/24
- 10.244.0.0/16
- 10.96.0.0/12
@@ -1,25 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: strimzi-operator
namespace: cicd
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
project: kmsvc
source:
repoURL: https://strimzi.io/charts/
chart: strimzi-kafka-operator
targetRevision: 0.46.0
helm:
values: |
watchNamespaces: ["sqs"]
destination:
server: https://kubernetes.default.svc
namespace: sqs
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
@@ -1,31 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: kafka-cluster
namespace: cicd
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
project: kmsvc
source:
repoURL: https://forgejo.riotpiao.com/rock/kafaka-management-service.git
targetRevision: main
path: k8s/charts/kafka-cluster
helm:
values: |
namespace: sqs
nodePool:
replicas: 3
storage:
class: longhorn
sizeGi: 50
resources:
memory: 5Gi
cpu: "2"
destination:
server: https://kubernetes.default.svc
namespace: sqs
syncPolicy:
automated:
prune: true
selfHeal: true
@@ -1,43 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: kmsvc-redis
namespace: cicd
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
project: kmsvc
source:
repoURL: https://charts.bitnami.com/bitnami
chart: redis
targetRevision: 20.6.0
helm:
values: |
architecture: standalone
# docker.io/bitnami stopped publishing version-pinned tags; bitnamilegacy
# mirrors them for free. allowInsecureImages silences the chart's
# container-image allowlist check, which doesn't know about that mirror.
global:
security:
allowInsecureImages: true
image:
repository: bitnamilegacy/redis
auth:
enabled: false
master:
persistence:
enabled: true
storageClass: longhorn
size: 2Gi
resources:
limits:
memory: 1Gi
requests:
memory: 1Gi
destination:
server: https://kubernetes.default.svc
namespace: sqs
syncPolicy:
automated:
prune: true
selfHeal: true
@@ -1,30 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: queue-crd
namespace: cicd
annotations:
argocd.argoproj.io/sync-wave: "2"
spec:
project: kmsvc
source:
repoURL: https://forgejo.riotpiao.com/rock/kafaka-management-service.git
targetRevision: main
path: k8s/charts/queue-crd
helm:
values: |
namespace: sqs
kafkaBrokers: "kmsvc-kafka-bootstrap.sqs.svc.cluster.local:9092"
redisAddr: "kmsvc-redis-master.sqs.svc.cluster.local:6379"
image:
repository: forgejo.riotpiao.com/rock/kafka-management-service-queue-operator
# CI (.forgejo/workflows/release.yaml) writes the released git tag
# here and pushes the commit -- ArgoCD picks it up on its next sync.
tag: latest
destination:
server: https://kubernetes.default.svc
namespace: sqs
syncPolicy:
automated:
prune: true
selfHeal: true
@@ -1,37 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: management-service
namespace: cicd
annotations:
argocd.argoproj.io/sync-wave: "2"
spec:
project: kmsvc
source:
repoURL: https://forgejo.riotpiao.com/rock/kafaka-management-service.git
targetRevision: main
path: k8s/charts/management-service
helm:
values: |
namespace: sqs
image:
repository: forgejo.riotpiao.com/rock/kafka-management-service
# CI (.forgejo/workflows/release.yaml) writes the released git tag
# here and pushes the commit -- ArgoCD picks it up on its next sync.
tag: latest
env:
kafkaBrokers: "kmsvc-kafka-bootstrap.sqs.svc.cluster.local:9092"
redisAddr: "kmsvc-redis-master.sqs.svc.cluster.local:6379"
authentikIssuerURL: "https://authentik.riotpiao.com/application/o/kafaka/"
authentikAudience: "QI0gPtR99ar8VvhK8Tqox4SDkTKzbNU7lbgwBNSc"
ingress:
enabled: true
host: kmsvc.riotpiao.com
clusterIssuer: homelab-ca
destination:
server: https://kubernetes.default.svc
namespace: sqs
syncPolicy:
automated:
prune: true
selfHeal: true
@@ -1,7 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: sqs
resources:
- 02-redis.yaml
- 03-queue-crd.yaml
- 04-management-service.yaml
@@ -1,7 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: sqs
resources:
- project.yaml
- root.yaml
- apps/
-23
View File
@@ -1,23 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: kmsvc
namespace: cicd
spec:
description: Kafka Management Service (design.md) -- Strimzi/Kafka, Redis, queue-operator, message-plane server
sourceRepos:
- https://forgejo.riotpiao.com/rock/kafaka-management-service.git
- https://strimzi.io/charts/
- https://charts.bitnami.com/bitnami
destinations:
- namespace: sqs
server: https://kubernetes.default.svc
- namespace: cicd
server: https://kubernetes.default.svc
clusterResourceWhitelist:
- group: "apiextensions.k8s.io"
kind: CustomResourceDefinition
- group: "rbac.authorization.k8s.io"
kind: ClusterRole
- group: "rbac.authorization.k8s.io"
kind: ClusterRoleBinding
-22
View File
@@ -1,22 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: kmsvc-root
namespace: cicd
spec:
project: kmsvc
source:
repoURL: https://forgejo.riotpiao.com/rock/kafaka-management-service.git
targetRevision: main
path: k8s/argocd/apps
directory:
recurse: false
destination:
server: https://kubernetes.default.svc
namespace: cicd
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
@@ -1,29 +0,0 @@
# design.md §7b: cluster-specific values for the homelab environment.
# No secrets here — Authentik client secret etc. flow through the existing
# Vault/talos-cli pattern, referenced at deploy time, not inlined.
namespace: sqs
kafkaCluster:
nodePool:
replicas: 3
storage:
# Use unified longhorn StorageClass (3 replicas, Immediate binding)
class: longhorn
# Per-node headroom: with 3 nodes and existing PVCs, estimate ~100+ Gi total
# available. Each node hosts one replica of all 3 broker volumes, so 3 *
# sizeGi must fit. Monitor usage during Kafka deployment.
sizeGi: 10
resources:
memory: 5Gi
cpu: "2"
redis:
storageClass: longhorn
memoryLimit: 1Gi
managementService:
ingress:
host: kmsvc.riotpiao.com
clusterIssuer: homelab-ca
authentikIssuerURL: "https://authentik.riotpiao.com/application/o/kafaka/"
authentikAudience: "QI0gPtR99ar8VvhK8Tqox4SDkTKzbNU7lbgwBNSc"
@@ -1,5 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: sqs
resources: []
# homelab.yaml is configuration, not a K8s manifest
-99
View File
@@ -1,99 +0,0 @@
environments:
default:
values:
- environments/homelab.yaml
homelab:
values:
- environments/homelab.yaml
---
helmDefaults:
wait: true
timeout: 600
repositories:
- name: strimzi
url: https://strimzi.io/charts/
- name: bitnami
url: https://charts.bitnami.com/bitnami
releases:
- name: strimzi-operator
namespace: {{ .Values.namespace }}
chart: strimzi/strimzi-kafka-operator
version: 0.46.0
values:
- watchNamespaces: ["{{ .Values.namespace }}"]
- name: kafka-cluster
namespace: {{ .Values.namespace }}
chart: charts/kafka-cluster
needs:
- {{ .Values.namespace }}/strimzi-operator
values:
- namespace: {{ .Values.namespace }}
nodePool:
replicas: {{ .Values.kafkaCluster.nodePool.replicas }}
storage:
class: {{ .Values.kafkaCluster.nodePool.storage.class }}
sizeGi: {{ .Values.kafkaCluster.nodePool.storage.sizeGi }}
resources:
memory: {{ .Values.kafkaCluster.nodePool.resources.memory }}
cpu: {{ .Values.kafkaCluster.nodePool.resources.cpu | quote }}
- name: kmsvc-redis
namespace: {{ .Values.namespace }}
chart: bitnami/redis
version: 20.6.0
values:
- architecture: standalone
# Bitnami stopped publishing version-pinned tags under docker.io/bitnami
# (only `latest` remains there); bitnamilegacy/* mirrors the old
# versioned tags for free, so pin there instead of floating on `latest`.
# The chart's container-image allowlist check doesn't know about the
# legacy mirror, hence allowInsecureImages.
global:
security:
allowInsecureImages: true
image:
repository: bitnamilegacy/redis
auth:
enabled: false
master:
persistence:
enabled: true
storageClass: {{ .Values.redis.storageClass }}
size: 2Gi
resources:
limits:
memory: {{ .Values.redis.memoryLimit }}
requests:
memory: {{ .Values.redis.memoryLimit }}
- name: queue-crd
namespace: {{ .Values.namespace }}
chart: charts/queue-crd
needs:
- {{ .Values.namespace }}/kafka-cluster
- {{ .Values.namespace }}/kmsvc-redis
values:
- namespace: {{ .Values.namespace }}
kafkaBrokers: "kmsvc-kafka-bootstrap.{{ .Values.namespace }}.svc.cluster.local:9092"
redisAddr: "kmsvc-redis-master.{{ .Values.namespace }}.svc.cluster.local:6379"
- name: management-service
namespace: {{ .Values.namespace }}
chart: charts/management-service
needs:
- {{ .Values.namespace }}/kafka-cluster
- {{ .Values.namespace }}/kmsvc-redis
values:
- namespace: {{ .Values.namespace }}
env:
kafkaBrokers: "kmsvc-kafka-bootstrap.{{ .Values.namespace }}.svc.cluster.local:9092"
redisAddr: "kmsvc-redis-master.{{ .Values.namespace }}.svc.cluster.local:6379"
authentikIssuerURL: {{ .Values.managementService.authentikIssuerURL | quote }}
authentikAudience: {{ .Values.managementService.authentikAudience | quote }}
ingress:
enabled: true
host: {{ .Values.managementService.ingress.host | quote }}
clusterIssuer: {{ .Values.managementService.ingress.clusterIssuer | quote }}
-8
View File
@@ -1,8 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: sqs
resources:
- argocd/
- environments/
- queues/
# Helm charts deployed via ArgoCD Helm source
@@ -1,5 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- orders-fifo.yaml
@@ -1,32 +0,0 @@
apiVersion: kmsvc.io/v1
kind: Queue
metadata:
name: orders-fifo
namespace: sqs
spec:
fifoQueue: true
visibilityTimeoutSeconds: 30
messageRetentionPeriodSeconds: 345600
maxReceiveCount: 5
deadLetterTargetQueue: orders-fifo-dlq
delaySeconds: 0
partitionsPerShard: 6
minShards: 1
maxShards: 8
shardSplitThresholdBytesPerSec: 5242880
shardSplitCooldownSeconds: 300
---
apiVersion: kmsvc.io/v1
kind: Queue
metadata:
name: orders-fifo-dlq
namespace: sqs
spec:
fifoQueue: true
isDLQ: true
visibilityTimeoutSeconds: 30
messageRetentionPeriodSeconds: 1209600
maxReceiveCount: 5
partitionsPerShard: 6
minShards: 1
maxShards: 1
@@ -1,237 +0,0 @@
# Phase 2: Namespace-Scoped Auto-Provisioning Testing Guide
## Overview
Phase 2 implements **namespace-scoped automatic TemporalWorker provisioning** (Kafka broker model).
One TemporalWorker per Temporal namespace processes ALL task queues in that namespace. When multiple Queues share the same `temporal.io/namespace` label, they trigger creation of a single TemporalWorker that handles all of them.
```
Queues (labeled temporal.io/namespace: "production")
├── orders-fifo
├── payments
└── notifications
queue-operator creates 1 TemporalWorker (worker-production)
TemporalWorker controller creates 1 Deployment
Worker pod(s) connect to Temporal namespace "production"
Process ALL task queues in that namespace (scale horizontally by replicas)
```
## Implementation Changes
### 1. TemporalWorker CRD (`apis/kmsvc/v1/temporalworker_types.go`)
- New Kubernetes resource type to manage namespace-scoped workers
- Fields: Namespace (required), Image, Replicas, Resources, NodeSelector, Affinity, Tolerations
- Status: Phase (Pending/Ready/Failed), Replicas, ReadyReplicas, Conditions
- Model: 1 TemporalWorker per Temporal namespace (not per queue)
### 2. QueueReconciler Extension (`internal/operator/queue_controller.go`)
- New method: `reconcileTemporalWorker()`
- Logic: If Queue has `temporal.io/namespace` label, create TemporalWorker for that namespace
- Idempotent: multiple queues with same namespace label create same TemporalWorker (no duplicates)
### 3. TemporalWorkerReconciler (`internal/operator/temporal_worker_controller.go`)
- New controller watching TemporalWorker objects
- Creates/updates Kubernetes Deployment with:
- Pod spec: container image, env vars (TEMPORAL_FRONTEND_ADDRESS, TEMPORAL_TASK_QUEUE)
- Replicas, resources, node selector, affinity, tolerations from TemporalWorker spec
- Updates TemporalWorker status with deployment replica counts and phase
### 4. Operator Main (`cmd/queue-operator/main.go`)
- Registers TemporalWorker CRD in scheme
- Registers TemporalWorkerReconciler controller
- Controller watches TemporalWorker objects; owns Deployment objects
## Testing Procedure
### Prerequisites
- kmsvc queue-operator must be running (built and deployed)
- Temporal cluster must be ready (temporal-frontend service available at `temporal-frontend.temporal.svc.cluster.local:7233`)
- story-crater-backend Docker image must exist (used as default worker image)
### Step 1: Build and Deploy kmsvc Operator
```bash
cd /Users/rockliang/workplace/kmsvc-manage
make build # builds queue-operator binary
make docker-build # builds Docker image
make deploy # deploys to cluster (requires Helm chart)
```
Or manually:
```bash
cd /Users/rockliang/workplace/kmsvc-manage
go build -o bin/queue-operator ./cmd/queue-operator
kubectl apply -f k8s/queue-operator-rbac.yaml
kubectl apply -f k8s/queue-operator-deployment.yaml
```
### Step 2: Create Queues with Temporal Namespace Labels
```bash
kubectl apply -f /Users/rockliang/workplace/homelab/k8s/temporal/queues/example-queue.yaml
```
Verify Queues are Ready:
```bash
kubectl get queue -n sqs -l temporal.io/namespace=production
kubectl describe queue -n sqs story-crater-tasks
```
Expected:
```
NAME FIFO PHASE AGE
story-crater-tasks false Ready 5s
story-crater-notifications false Ready 5s
```
### Step 3: Verify TemporalWorker CRD Auto-Created (1 per namespace)
```bash
kubectl get temporalworker -n temporal
kubectl describe temporalworker -n temporal worker-production
```
Expected:
```
NAME PHASE READY DESIRED AGE
worker-production Pending 0 1 5s
```
Only ONE TemporalWorker for all queues in "production" namespace!
### Step 4: Verify Deployment Auto-Created
```bash
kubectl get deploy -n temporal -l app.kubernetes.io/managed-by=kmsvc-temporal-operator
kubectl get pods -n temporal -l app.kubernetes.io/instance=worker-production
```
Expected:
```
NAME READY UP-TO-DATE AVAILABLE AGE
worker-production 1/1 1 1 10s
NAME READY STATUS RESTARTS AGE
worker-production-5f8b4c... 1/1 Running 0 10s
```
### Step 5: Verify Worker Connected to Temporal Namespace
Check Temporal UI for namespace "production":
```bash
open https://temporal.riotpiao.com/namespaces/production/task-queues
```
Look for all task queues with worker count > 0:
- `story-crater-tasks`
- `story-crater-notifications`
- (worker processes all of them)
Or via CLI:
```bash
kubectl port-forward -n temporal svc/temporal-frontend 7233 &
curl http://localhost:7233/api/v1/task-queues?namespace=production
```
### Step 6: Verify TemporalWorker Status Updated
```bash
kubectl get temporalworker -n temporal
kubectl describe temporalworker -n temporal worker-production
```
Expected:
```
NAME PHASE READY DESIRED AGE
worker-production Ready 1 1 15s
Status:
Phase: Ready
Ready Replicas: 1
Replicas: 1
```
### Step 7: Test Namespace-Level Scaling
Create more queues in the same namespace:
```yaml
apiVersion: kmsvc.io/v1
kind: Queue
metadata:
name: story-crater-llm-processing
namespace: sqs
labels:
temporal.io/namespace: "production" # same namespace
```
Verify: No new TemporalWorker created (same worker handles all 3 queues):
```bash
kubectl get temporalworker -n temporal # still just 1 worker-production
kubectl get deploy -n temporal worker-production # same deployment
```
Worker auto-discovers new task queue in namespace and processes it.
### Step 8: Test Cascading Deletion
Delete a Queue; worker should remain (other queues still need it):
```bash
kubectl delete queue -n sqs story-crater-notifications
```
Verify:
```bash
kubectl get temporalworker -n temporal # worker-production still exists
kubectl get pods -n temporal worker-production # still running
```
Delete all queues in namespace:
```bash
kubectl delete queue -n sqs -l temporal.io/namespace=production
```
Verify: TemporalWorker now has no owner (not cascade-deleted; manual cleanup needed):
```bash
kubectl get temporalworker -n temporal # worker-production still there (manual cleanup)
kubectl delete temporalworker -n temporal worker-production # cleanup manually
```
## Debugging
### Queue stuck in Pending
Check queue-operator logs:
```bash
kubectl logs -n sqs deploy/kmsvc-queue-operator -f
kubectl logs -n sqs deploy/kmsvc-queue-operator --tail=50 | grep -i error
```
### TemporalWorker not created
- Verify Queue has the label: `kubectl get queue -o yaml | grep temporal.io`
- Check queue-operator logs for "reconcileTemporalWorker" errors
### Deployment not created
- Check TemporalWorker controller logs: `kubectl logs -n sqs deploy/kmsvc-queue-operator -f`
- Verify TemporalWorker exists: `kubectl get temporalworker -n temporal`
- Check Deployment errors: `kubectl describe deploy -n temporal worker-story-crater-tasks`
### Worker not showing in Temporal UI
- Check pod logs: `kubectl logs -n temporal deploy/worker-story-crater-tasks`
- Verify env vars: `kubectl set env pod -n temporal <pod-name> --list | grep TEMPORAL`
- Test connectivity: `kubectl exec -n temporal <pod-name> -- nc -zv temporal-frontend.temporal.svc.cluster.local 7233`
## Next Steps
Once Phase 2 is working:
1. **Phase 3 (Future):** Implement autoscaling based on queue depth metrics
2. **Production Hardening:**
- Add QueueRef validation (ensure Queue exists in sqs namespace)
- Add image validation/defaults from ConfigMap
- Add worker readiness probe configuration
- Add graceful shutdown/drain behavior
## Files Modified/Created
| File | Change |
|------|--------|
| `apis/kmsvc/v1/temporalworker_types.go` | NEW: CRD type definitions |
| `internal/operator/queue_controller.go` | MODIFIED: Added reconcileTemporalWorker() |
| `internal/operator/temporal_worker_controller.go` | NEW: TemporalWorker → Deployment reconciler |
| `cmd/queue-operator/main.go` | MODIFIED: Register TemporalWorker CRD + controller |
| `k8s/temporal/queues/example-queue.yaml` | NEW: Example Queue with label |
@@ -1,220 +0,0 @@
# Temporal OAuth2-Proxy Setup (Authentik OIDC)
## Overview
Protects Temporal UI with Authentik OIDC authentication. Traffic flow:
```
Browser → Ingress (TLS) → oauth2-proxy (OIDC check) → temporal-web (internal)
Redirects to Authentik login
JWT cookie issued
Forwards to temporal-web
```
## Prerequisites
✅ Authentik OIDC provider `temporal` already exists with:
- Client ID: `temporal`
- Client Secret: stored in Kubernetes secret `temporal-oidc` (key: `clientSecret`)
- Redirect URI: `https://temporal.riotpiao.com/oauth2/callback`
## Secrets
The `temporal-oidc` secret must contain:
| Key | Value | Source |
|-----|-------|--------|
| `clientSecret` | OAuth2 client secret from Authentik | Authentik → Applications → temporal |
| `cookieSecret` | Session encryption key (base64 32-byte) | Generate: `openssl rand -base64 32` |
### Check existing secret:
```bash
kubectl get secret -n temporal temporal-oidc
kubectl describe secret -n temporal temporal-oidc
```
### If missing, create it:
```bash
# Get client secret from Authentik UI
# Applications → temporal → copy "Client Secret"
CLIENT_SECRET="..."
# Generate cookie secret
COOKIE_SECRET=$(openssl rand -base64 32)
# Create secret
kubectl create secret generic temporal-oidc \
-n temporal \
--from-literal=clientSecret="${CLIENT_SECRET}" \
--from-literal=cookieSecret="${COOKIE_SECRET}"
```
## Deployment Steps
### Step 1: Apply OAuth2-Proxy Manifests
```bash
kubectl apply -f k8s/temporal/oauth2-proxy.yaml
```
Verify:
```bash
kubectl get deploy -n temporal oauth2-proxy
kubectl logs -n temporal deploy/oauth2-proxy
```
Expected log:
```
[<timestamp>] [oauthproxy.go:...] Listening on 0.0.0.0:4180
```
### Step 2: Apply OAuth2-Proxy Ingress
```bash
kubectl apply -f k8s/temporal/temporal-ingress-oauth2.yaml
```
Verify:
```bash
kubectl get ingress -n temporal
```
Expected:
```
NAME CLASS HOSTS ADDRESS PORTS AGE
temporal nginx temporal.riotpiao.com ... 80, 443 10s
```
### Step 3: Test Access
1. **Open Temporal UI (unauthenticated):**
```bash
open https://temporal.riotpiao.com
```
Expected: Redirects to Authentik login page
2. **Login with Authentik credentials**
- Username/email
- Password
- Should redirect back to `temporal.riotpiao.com` and display UI
3. **Verify auth:**
```bash
# Check for oauth2_proxy cookie
curl -v https://temporal.riotpiao.com 2>&1 | grep -i cookie
```
4. **Check oauth2-proxy logs:**
```bash
kubectl logs -n temporal deploy/oauth2-proxy -f
```
Look for:
```
[timestamp] [auth_test.go:...] Authentication successful
```
## Troubleshooting
### Redirect URI mismatch
Error in oauth2-proxy logs:
```
redirect_uri_mismatch: The redirect_uri does not match the one registered in Authentik
```
Fix:
- Verify Authentik application (Applications → temporal) has redirect URI: `https://temporal.riotpiao.com/oauth2/callback`
- Ensure HTTPS (not HTTP)
### Missing secret
Error:
```
clientSecret: key not found in temporal-oidc secret
```
Fix:
```bash
kubectl get secret -n temporal temporal-oidc -o yaml
# If missing, create per "Secrets" section above
```
### Cookie secret expiration
OAuth2-Proxy won't start if `cookieSecret` is empty or invalid.
Fix:
```bash
COOKIE_SECRET=$(openssl rand -base64 32)
kubectl patch secret temporal-oidc -n temporal \
-p "{\"data\":{\"cookieSecret\":\"$(echo -n $COOKIE_SECRET | base64)\"}}}"
kubectl rollout restart deploy/oauth2-proxy -n temporal
```
### oauth2-proxy crashes with "connection refused"
Error in logs:
```
upstream connect error or disconnect/reset before headers
```
Likely cause: `temporal-web` service not accessible.
Check:
```bash
kubectl get svc -n temporal temporal-web
kubectl exec -n temporal deploy/oauth2-proxy -- curl http://temporal-web:8080
```
## File Structure
```
k8s/temporal/
├── oauth2-proxy.yaml # oauth2-proxy Deployment + Service + SA
├── temporal-ingress-oauth2.yaml # Ingress routing to oauth2-proxy
├── oauth2-proxy-values.yaml # Helm values (reference only)
└── temporal-values.yaml # Modified: ingress.enabled=false
```
## Next: Add to Helmfile
If integrating with helmfile.yaml.gotmpl:
```yaml
releases:
- name: temporal
# ... existing config ...
hooks:
postSync:
- events: ["success"]
showlogs: true
command: "sh"
args:
- -c
- |
kubectl apply -f k8s/temporal/oauth2-proxy.yaml
kubectl apply -f k8s/temporal/temporal-ingress-oauth2.yaml
```
Or add separate releases:
```yaml
- name: oauth2-proxy-temporal
namespace: temporal
chart: oauth2-proxy/oauth2-proxy
version: "6.x.x"
values:
- k8s/temporal/oauth2-proxy-values.yaml
set:
- name: config.clientSecret
value: "{{ (env "TEMPORAL_OIDC_CLIENT_SECRET") }}"
- name: config.cookieSecret
value: "{{ (env "TEMPORAL_OIDC_COOKIE_SECRET") }}"
```
Then add to `.env`:
```bash
TEMPORAL_OIDC_CLIENT_SECRET=<from Authentik>
TEMPORAL_OIDC_COOKIE_SECRET=$(openssl rand -base64 32)
```
@@ -1,102 +0,0 @@
# Elasticsearch 7.17.0 for Temporal visibility store
# Deployed to worker nodes (not control plane to save CP resources for LLM work)
# 2Gi heap + 4Gi memory limit for stable operation
apiVersion: v1
kind: ConfigMap
metadata:
name: elasticsearch-config
namespace: temporal
data:
elasticsearch.yml: |
cluster.name: temporal-elasticsearch
node.name: temporal-elasticsearch-0
discovery.type: single-node
network.host: 0.0.0.0
http.host: 0.0.0.0
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: temporal-elasticsearch
namespace: temporal
spec:
replicas: 1
selector:
matchLabels:
app: temporal-elasticsearch
template:
metadata:
labels:
app: temporal-elasticsearch
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/worker
operator: Exists
containers:
- name: elasticsearch
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0
env:
- name: discovery.type
value: single-node
- name: "ES_JAVA_OPTS"
value: "-Xms2g -Xmx2g"
ports:
- containerPort: 9200
name: http
- containerPort: 9300
name: transport
livenessProbe:
httpGet:
path: /_cluster/health
port: 9200
initialDelaySeconds: 180
periodSeconds: 10
timeoutSeconds: 10
failureThreshold: 5
readinessProbe:
httpGet:
path: /_cluster/health?local=true
port: 9200
initialDelaySeconds: 150
periodSeconds: 10
timeoutSeconds: 10
failureThreshold: 5
resources:
requests:
cpu: 500m
memory: 2Gi
limits:
cpu: 2000m
memory: 4Gi
volumeMounts:
- name: config
mountPath: /usr/share/elasticsearch/config/elasticsearch.yml
subPath: elasticsearch.yml
volumes:
- name: config
configMap:
name: elasticsearch-config
---
apiVersion: v1
kind: Service
metadata:
name: temporal-elasticsearch
namespace: temporal
spec:
selector:
app: temporal-elasticsearch
ports:
- port: 9200
targetPort: 9200
name: http
- port: 9300
targetPort: 9300
name: transport
type: ClusterIP
@@ -1,8 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: temporal
resources:
- elasticsearch.yaml
- queues/
# SOPS secrets (*.enc.yaml) handled by ArgoCD SOPS plugin at sync time
# temporal deployed via ArgoCD Helm source
@@ -1,38 +0,0 @@
# Example Queues for "production" Temporal namespace
# When applied, queue-operator creates 1 TemporalWorker (worker-production)
# that processes ALL queues in the "production" namespace
---
apiVersion: kmsvc.io/v1
kind: Queue
metadata:
name: story-crater-tasks
namespace: sqs
labels:
temporal.io/namespace: "production"
spec:
fifoQueue: false
visibilityTimeoutSeconds: 30
messageRetentionPeriodSeconds: 345600
maxReceiveCount: 5
partitionsPerShard: 6
minShards: 1
maxShards: 8
shardSplitThresholdBytesPerSec: 5242880
---
apiVersion: kmsvc.io/v1
kind: Queue
metadata:
name: story-crater-notifications
namespace: sqs
labels:
temporal.io/namespace: "production"
spec:
fifoQueue: false
visibilityTimeoutSeconds: 60
messageRetentionPeriodSeconds: 345600
maxReceiveCount: 3
partitionsPerShard: 3
minShards: 1
maxShards: 4
shardSplitThresholdBytesPerSec: 2621440
@@ -1,5 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- example-queue.yaml
@@ -1,6 +1,6 @@
# k8s/temporal/temporal-values.yaml # k8s/temporal/temporal-values.yaml
# Temporal — workflow engine # Temporal — workflow engine
# Uses external CNPG PostgreSQL for persistence (ddb-cluster) # Uses external CNPG PostgreSQL for persistence (temporal-db)
# Visibility via same PostgreSQL instance, separate database. # Visibility via same PostgreSQL instance, separate database.
# #
# IMPORTANT — chart schema note (root-caused after Postgres never actually # IMPORTANT — chart schema note (root-caused after Postgres never actually
@@ -43,11 +43,11 @@ grafana:
enabled: false enabled: false
# ── Schema setup/update Jobs ────────────────────────────────────────────────── # ── Schema setup/update Jobs ──────────────────────────────────────────────────
# The `temporal` and `temporal_visibility` databases are provisioned # The `temporal` DB is created by the dedicated temporal-db cluster's initdb and
# declaratively by CNPG Database CRs (k8s/data/temporal-database.yaml, # `temporal_visibility` by a CNPG Database CR — both in
# temporal-visibility-database.yaml), so createDatabase stays disabled (the # k8s/infra/databases/temporal-db.yaml so createDatabase stays disabled.
# `temporal` role also lacks CREATEDB). setup/update run temporal-sql-tool as # setup/update run temporal-sql-tool as the `app` owner against those existing
# the `temporal` owner against those existing DBs to install and migrate the # DBs to install and migrate the
# Temporal server schema — without them both DBs have zero tables and 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). # server dies on "no usable database connection found" (no schema_version row).
schema: schema:
@@ -92,7 +92,7 @@ server:
driver: "sql" driver: "sql"
sql: sql:
driver: "postgres12" driver: "postgres12"
host: "ddb-cluster-rw.ddb.svc.cluster.local" host: "temporal-db-rw.temporal.svc.cluster.local"
port: 5432 port: 5432
database: "temporal" database: "temporal"
user: "app" user: "app"
@@ -102,8 +102,8 @@ server:
# existingSecret is set the chart's own server-secret.yaml Secret # existingSecret is set the chart's own server-secret.yaml Secret
# template is skipped entirely (see templates/server-secret.yaml: # template is skipped entirely (see templates/server-secret.yaml:
# `not $driverConfig.existingSecret` guards its creation). # `not $driverConfig.existingSecret` guards its creation).
# Use unified ddb-cluster-app secret (copied to temporal namespace) # Use unified temporal-db-app secret (generated in temporal namespace)
existingSecret: "ddb-cluster-app" existingSecret: "temporal-db-app"
secretKey: "password" secretKey: "password"
maxConns: 20 maxConns: 20
maxIdleConns: 10 maxIdleConns: 10
@@ -118,12 +118,12 @@ server:
driver: "sql" driver: "sql"
sql: sql:
driver: "postgres12" driver: "postgres12"
host: "ddb-cluster-rw.ddb.svc.cluster.local" host: "temporal-db-rw.temporal.svc.cluster.local"
port: 5432 port: 5432
database: "temporal_visibility" database: "temporal_visibility"
user: "app" user: "app"
# Use unified ddb-cluster-app secret (copied to temporal namespace) # Use unified temporal-db-app secret (generated in temporal namespace)
existingSecret: "ddb-cluster-app" existingSecret: "temporal-db-app"
secretKey: "password" secretKey: "password"
maxConns: 20 maxConns: 20
maxIdleConns: 10 maxIdleConns: 10
+39 -8
View File
@@ -16,7 +16,7 @@ spec:
targetRevision: "5.0.18" targetRevision: "5.0.18"
helm: helm:
valueFiles: valueFiles:
- $values/k8s/infrastructure/minio/minio-operator-values.yaml - $values/k8s/infra/minio/minio-operator-values.yaml
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git - repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
ref: values ref: values
@@ -43,7 +43,7 @@ spec:
source: source:
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
path: k8s/infrastructure/minio path: k8s/infra/minio
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: storage namespace: storage
@@ -68,7 +68,7 @@ spec:
source: source:
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
path: k8s/infrastructure/longhorn path: k8s/infra/longhorn
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: longhorn-system namespace: longhorn-system
@@ -93,7 +93,7 @@ spec:
helm: helm:
skipCrds: true skipCrds: true
valueFiles: valueFiles:
- $values/k8s/platform/monitoring/prometheus-values.yaml - $values/k8s/infra/monitoring/prometheus-values.yaml
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git - repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
ref: values ref: values
@@ -122,7 +122,7 @@ spec:
# metadata conflict since CRDs are cluster-scoped). # metadata conflict since CRDs are cluster-scoped).
--- ---
# CRDs only, extracted to plain YAML (`helm show crds kube-prometheus-stack`) # CRDs only, extracted to plain YAML (`helm show crds kube-prometheus-stack`)
# and committed to git under k8s/platform/monitoring/crds/, applied via Server- # and committed to git under k8s/infra/monitoring/crds/, applied via Server-
# Side Apply to avoid the etcd 262144-byte last-applied-configuration # Side Apply to avoid the etcd 262144-byte last-applied-configuration
# annotation limit that client-side apply hits on these very large CRDs # annotation limit that client-side apply hits on these very large CRDs
# (prometheuses, alertmanagers, scrapeconfigs, etc). A plain git path source # (prometheuses, alertmanagers, scrapeconfigs, etc). A plain git path source
@@ -131,7 +131,7 @@ spec:
# means from a Helm chart. Split out from the main `prometheus` Application # means from a Helm chart. Split out from the main `prometheus` Application
# (helm.skipCrds: true there) because ServerSideApply conflicts with that # (helm.skipCrds: true there) because ServerSideApply conflicts with that
# app's managedNamespaceMetadata. # app's managedNamespaceMetadata.
# NOTE: bump k8s/platform/monitoring/crds/kube-prometheus-stack-crds.yaml # NOTE: bump k8s/infra/monitoring/crds/kube-prometheus-stack-crds.yaml
# whenever the kube-prometheus-stack chart version changes materially # whenever the kube-prometheus-stack chart version changes materially
# (`helm show crds prometheus-community/kube-prometheus-stack > ...`). # (`helm show crds prometheus-community/kube-prometheus-stack > ...`).
apiVersion: argoproj.io/v1alpha1 apiVersion: argoproj.io/v1alpha1
@@ -146,7 +146,38 @@ spec:
source: source:
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
path: k8s/platform/monitoring/crds path: k8s/infra/monitoring/crds
destination:
server: https://kubernetes.default.svc
namespace: monitoring
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
---
# Cluster monitoring config: custom PrometheusRules (per-app namespaces),
# ServiceMonitors (monitoring ns), and Grafana dashboard ConfigMaps (logging ns,
# grafana sidecar-discovered). Single source = k8s/infra/monitoring (one
# kustomization, no namespace transformer so per-app rule namespaces are kept).
# Wave 2: after prometheus-operator CRDs (wave 0) + stack (wave 1) and grafana
# (wave 2, logging). ServerSideApply avoids the etcd last-applied annotation
# limit on the large dashboard ConfigMap JSON.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: monitoring-config
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "2"
spec:
project: homelab
source:
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main
path: k8s/infra/monitoring
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: monitoring namespace: monitoring
@@ -173,7 +204,7 @@ spec:
targetRevision: "~11" targetRevision: "~11"
helm: helm:
valueFiles: valueFiles:
- $values/k8s/platform/monitoring/blackbox-exporter-values.yaml - $values/k8s/infra/monitoring/blackbox-exporter-values.yaml
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git - repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
ref: values ref: values
+3 -3
View File
@@ -18,7 +18,7 @@ spec:
targetRevision: "*" targetRevision: "*"
helm: helm:
valueFiles: valueFiles:
- $values/k8s/platform/logging/loki-values.yaml - $values/k8s/infra/logging/loki-values.yaml
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git - repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
ref: values ref: values
@@ -52,7 +52,7 @@ spec:
targetRevision: "*" targetRevision: "*"
helm: helm:
valueFiles: valueFiles:
- $values/k8s/platform/logging/grafana-values.yaml - $values/k8s/infra/logging/grafana-values.yaml
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git - repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
ref: values ref: values
@@ -86,7 +86,7 @@ spec:
targetRevision: "*" targetRevision: "*"
helm: helm:
valueFiles: valueFiles:
- $values/k8s/platform/logging/promtail-values.yaml - $values/k8s/infra/logging/promtail-values.yaml
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git - repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
ref: values ref: values
+5 -5
View File
@@ -1,6 +1,6 @@
# Wave 3 — Vault + Authentik (identity), plus IAM raw jobs and the Forgejo # Wave 3 — Vault + Authentik (identity), plus IAM raw jobs and the Forgejo
# runner. Authentik/Vault values reference SOPS-managed secrets (see *.enc.yaml # runner. Authentik/Vault values reference SOPS-managed secrets (see *.enc.yaml
# in k8s/security/iam) resolved by the ArgoCD SOPS plugin at sync time. # in k8s/infra/iam) resolved by the ArgoCD SOPS plugin at sync time.
apiVersion: argoproj.io/v1alpha1 apiVersion: argoproj.io/v1alpha1
kind: Application kind: Application
metadata: metadata:
@@ -16,7 +16,7 @@ spec:
targetRevision: "*" targetRevision: "*"
helm: helm:
valueFiles: valueFiles:
- $values/k8s/security/iam/vault-values.yaml - $values/k8s/infra/iam/vault-values.yaml
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git - repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
ref: values ref: values
@@ -45,7 +45,7 @@ spec:
targetRevision: "*" targetRevision: "*"
helm: helm:
valueFiles: valueFiles:
- $values/k8s/security/iam/authentik-values.yaml - $values/k8s/infra/iam/authentik-values.yaml
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git - repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
ref: values ref: values
@@ -70,7 +70,7 @@ spec:
source: source:
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
path: k8s/security/iam path: k8s/infra/iam
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: iam namespace: iam
@@ -92,7 +92,7 @@ spec:
source: source:
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
path: k8s/security/ci-cd/charts/forgejo-runner path: k8s/infra/forgejo-runner
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: cicd namespace: cicd
+12 -8
View File
@@ -1,24 +1,28 @@
# Wave 6Database schemas + init jobs. # Wave 2dedicated per-app CNPG Postgres clusters (authentik-db → ns iam,
# CNPG operator and ddb-cluster are bootstrap-only (k8s/bootstrap-local/). # temporal-db + visibility → ns temporal). ONE App, ONE folder (k8s/infra/databases).
# This app manages ONLY the per-app databases and schema initialization. # CNPG operator is Phase-0 bootstrap; these Cluster CRs are GitOps — no circular
# Dependencies: ddb-cluster (bootstrap wave 0), SOPS secrets (wave 4) # 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 apiVersion: argoproj.io/v1alpha1
kind: Application kind: Application
metadata: metadata:
name: data-schemas name: databases
namespace: argocd namespace: argocd
annotations: annotations:
argocd.argoproj.io/sync-wave: "6" argocd.argoproj.io/sync-wave: "2"
spec: spec:
project: homelab project: homelab
source: source:
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
path: k8s/data/schemas # CHANGED from k8s/data to avoid ddb-cluster duplication path: k8s/infra/databases
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: ddb namespace: default
syncPolicy: syncPolicy:
automated: automated:
prune: true prune: true
selfHeal: true selfHeal: true
syncOptions:
- ServerSideApply=true
+3 -3
View File
@@ -70,7 +70,7 @@ spec:
source: source:
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
path: k8s/applications/sqs/charts/kafka-cluster path: k8s/apps/messaging/kafka-cluster
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: sqs namespace: sqs
@@ -91,7 +91,7 @@ spec:
source: source:
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
path: k8s/applications/sqs/charts/queue-crd path: k8s/apps/messaging/queue-crd
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: sqs namespace: sqs
@@ -112,7 +112,7 @@ spec:
source: source:
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
path: k8s/applications/sqs/charts/management-service path: k8s/apps/messaging/management-service
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: sqs namespace: sqs
+7 -7
View File
@@ -2,7 +2,7 @@
# helpers (cloudflared tunnel, duckdns updater) that are already running. # helpers (cloudflared tunnel, duckdns updater) that are already running.
# Experimental dirs (llm, forge, dev-tools, shadowsocks) are intentionally # Experimental dirs (llm, forge, dev-tools, shadowsocks) are intentionally
# NOT included yet — add them here once they're production-ready. # NOT included yet — add them here once they're production-ready.
# temporal using unified CNPG pattern (app user, ddb-cluster-app secret) # temporal using unified CNPG pattern (app user, temporal-db-app secret)
# Secret copied by bootstrap.sh (like cicd/iam namespaces) # Secret copied by bootstrap.sh (like cicd/iam namespaces)
apiVersion: argoproj.io/v1alpha1 apiVersion: argoproj.io/v1alpha1
kind: Application kind: Application
@@ -19,7 +19,7 @@ spec:
targetRevision: "0.74.0" targetRevision: "0.74.0"
helm: helm:
valueFiles: valueFiles:
- $values/k8s/applications/temporal/temporal-values.yaml - $values/k8s/apps/temporal/temporal-values.yaml
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git - repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
ref: values ref: values
@@ -48,7 +48,7 @@ spec:
targetRevision: "*" targetRevision: "*"
helm: helm:
valueFiles: valueFiles:
- $values/k8s/applications/portainer/portainer-values.yaml - $values/k8s/apps/portainer/portainer-values.yaml
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git - repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
ref: values ref: values
@@ -74,7 +74,7 @@ spec:
source: source:
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
path: k8s/applications/cloudflared path: k8s/apps/cloudflared
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: cloudflared namespace: cloudflared
@@ -97,7 +97,7 @@ spec:
source: source:
repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
path: k8s/applications/duckdns path: k8s/apps/duckdns
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: duckdns namespace: duckdns
@@ -125,13 +125,13 @@ spec:
targetRevision: "*" targetRevision: "*"
helm: helm:
valueFiles: valueFiles:
- $values/k8s/applications/homarr/homarr-values.yaml - $values/k8s/apps/homarr/homarr-values.yaml
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git - repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
ref: values ref: values
- repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git - repoURL: https://forgejo.riotpiao.com/riotpiao.com/homelab.git
targetRevision: main targetRevision: main
path: k8s/applications/homarr # PostSync hook: fix-probes-job.yaml path: k8s/apps/homarr # PostSync hook: fix-probes-job.yaml
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: dashboard namespace: dashboard
+1 -1
View File
@@ -115,7 +115,7 @@ git push forgejo main
- **Phase stuck?** Check `kubectl get events -n <namespace> --sort-by='.lastTimestamp'` - **Phase stuck?** Check `kubectl get events -n <namespace> --sort-by='.lastTimestamp'`
- **ArgoCD duplicating?** Verify manifests match exactly (Helm values ↔ ArgoCD Application) - **ArgoCD duplicating?** Verify manifests match exactly (Helm values ↔ ArgoCD Application)
- **Forgejo won't start?** Check CNPG cluster Ready: `kubectl get cluster forgejo-db -n forgejo` - **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` - **Can't push to Forgejo?** Verify ingress-nginx healthy, DNS resolves `forgejo.riotpiao.com`
## Migration from Old Bootstrap ## Migration from Old Bootstrap
+15 -5
View File
@@ -5,7 +5,7 @@
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
name: forgejo name: cicd
annotations: annotations:
argocd.argoproj.io/sync-options: Prune=false argocd.argoproj.io/sync-options: Prune=false
--- ---
@@ -13,7 +13,7 @@ apiVersion: postgresql.cnpg.io/v1
kind: Cluster kind: Cluster
metadata: metadata:
name: forgejo-db name: forgejo-db
namespace: forgejo namespace: cicd
annotations: annotations:
argocd.argoproj.io/sync-options: Prune=false # Let ArgoCD adopt, don't delete argocd.argoproj.io/sync-options: Prune=false # Let ArgoCD adopt, don't delete
labels: labels:
@@ -71,15 +71,21 @@ spec:
enablePodMonitor: true enablePodMonitor: true
affinity: affinity:
podAntiAffinityType: required # preferred (not required) so it can't deadlock if fewer than 3 nodes are
# schedulable; tolerations let CNPG pods land on control-plane nodes.
podAntiAffinityType: preferred
topologyKey: kubernetes.io/hostname topologyKey: kubernetes.io/hostname
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
--- ---
# Forgejo Redis (cache, session, queue) # Forgejo Redis (cache, session, queue)
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: forgejo-redis name: forgejo-redis
namespace: forgejo namespace: cicd
annotations: annotations:
argocd.argoproj.io/sync-options: Prune=false argocd.argoproj.io/sync-options: Prune=false
spec: spec:
@@ -95,7 +101,7 @@ apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: forgejo-redis name: forgejo-redis
namespace: forgejo namespace: cicd
annotations: annotations:
argocd.argoproj.io/sync-options: Prune=false argocd.argoproj.io/sync-options: Prune=false
spec: spec:
@@ -108,6 +114,10 @@ spec:
labels: labels:
app: forgejo-redis app: forgejo-redis
spec: spec:
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
containers: containers:
- name: redis - name: redis
image: redis:7-alpine image: redis:7-alpine

Some files were not shown because too many files have changed in this diff Show More