8 Commits

Author SHA1 Message Date
9325cd901f hc
All checks were successful
AI Code Review / ai-review (pull_request) Successful in 4s
2026-04-21 08:47:19 +02:00
077be9fbf3 cmd
All checks were successful
AI Code Review / ai-review (pull_request) Successful in 4s
2026-04-20 13:39:44 +02:00
16da2fa6b3 vars
Some checks failed
AI Code Review / ai-review (pull_request) Failing after 3s
2026-04-20 13:39:02 +02:00
9ab283f1e5 workflow fix
Some checks failed
AI Code Review / ai-review (pull_request) Failing after 9s
2026-04-20 13:37:40 +02:00
e06b270e67 pip
Some checks failed
AI Code Review / ai-review (pull_request) Failing after 8s
2026-04-20 13:11:30 +02:00
89d2952d7a flag
Some checks failed
AI Code Review / ai-review (pull_request) Failing after 8s
2026-04-20 13:09:06 +02:00
3d6eadf128 workflow fix
Some checks failed
AI Code Review / ai-review (pull_request) Failing after 9s
2026-04-20 13:07:50 +02:00
260b45637e AI-review
Some checks failed
AI Code Review / ai-review (pull_request) Failing after 8s
2026-04-20 13:02:48 +02:00
129 changed files with 2202 additions and 2398 deletions

2
.gitattributes vendored
View File

@@ -1,2 +0,0 @@
# Force LF line endings for shell scripts
*.sh text eol=lf

View File

@@ -2,12 +2,10 @@ name: AI Code Review
on: on:
pull_request: pull_request:
types: [ labeled, synchronize ] types: [ opened, synchronize ]
jobs: jobs:
ai-review: ai-review:
if: >-
(github.event.action == 'synchronized' && contains(toJSON(github.event.pull_request.labels), 'ai-review')) || contains(toJSON(gitea.event.changes.added_labels), 'ai-review')
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
@@ -19,14 +17,11 @@ jobs:
VCS__PIPELINE__PULL_NUMBER: ${{ github.event.pull_request.number }} VCS__PIPELINE__PULL_NUMBER: ${{ github.event.pull_request.number }}
VCS__HTTP_CLIENT__API_URL: https://git.forteapps.net/api/v1 VCS__HTTP_CLIENT__API_URL: https://git.forteapps.net/api/v1
VCS__HTTP_CLIENT__API_TOKEN: ${{ secrets.AI_REVIEW_TOKEN }} VCS__HTTP_CLIENT__API_TOKEN: ${{ secrets.AI_REVIEW_TOKEN }}
# Review — disable fallback to see real Gitea API errors
REVIEW__INLINE_COMMENT_FALLBACK: "false"
# LLM configuration # LLM configuration
LLM__PROVIDER: CLAUDE LLM__PROVIDER: CLAUDE
LLM__META__MODEL: claude-sonnet-4-20250514 LLM__META__MODEL: claude-sonnet-4-20250514
LLM__META__MAX_TOKENS: "4096"
LLM__HTTP_CLIENT__API_URL: https://api.anthropic.com LLM__HTTP_CLIENT__API_URL: https://api.anthropic.com
LLM__HTTP_CLIENT__API_TOKEN: ${{ secrets.ANTHROPIC_API_KEY }} LLM__HTTP_CLIENT__API_TOKEN: "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"
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -34,14 +29,13 @@ jobs:
with: with:
submodules: true submodules: true
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.AI_REVIEW_TOKEN }}
- name: Run inline review - name: Run inline review
uses: docker://nikitafilonov/ai-review:v0.64.0 uses: docker://nikitafilonov/ai-review:latest
with: with:
args: ai-review run-inline args: ai-review run-inline
- name: Run summary review - name: Run summary review
uses: docker://nikitafilonov/ai-review:v0.64.0 uses: docker://nikitafilonov/ai-review:latest
with: with:
args: ai-review run-summary args: ai-review run-summary

7
.project-standards.yaml Normal file
View File

@@ -0,0 +1,7 @@
standards_version: "2025.1"
last_configured: "2026-04-04"
components:
github-pages: "2025.1"
github-pages-generator: "mkdocs"
github-pages-source: "docs/"
github-pages-theme: "material"

View File

@@ -1,9 +1,9 @@
# Kubernetes Cluster - GitOps Configuration # Kubernetes Cluster - GitOps Configuration
> **Kubernetes cluster bootstrapping and GitOps configuration repository** using ArgoCD for multi-cloud Kubernetes (UpCloud, AWS EKS, Azure AKS, GCP GKE) > **Kubernetes cluster bootstrapping and GitOps configuration repository** using ArgoCD for UpCloud Managed Kubernetes
[![GitOps](https://img.shields.io/badge/GitOps-ArgoCD-blue)](https://argoproj.github.io/cd/) [![GitOps](https://img.shields.io/badge/GitOps-ArgoCD-blue)](https://argoproj.github.io/cd/)
[![Kubernetes](https://img.shields.io/badge/Kubernetes-Multi--Cloud-orange)]() [![Kubernetes](https://img.shields.io/badge/Kubernetes-UpCloud-orange)](https://upcloud.com/)
--- ---
@@ -95,26 +95,14 @@ This repository contains the complete GitOps configuration for our Kubernetes cl
│ │ ├── renovate.yaml │ │ ├── renovate.yaml
│ │ ├── ... # All other Application manifests │ │ ├── ... # All other Application manifests
│ │ └── secrets.yaml │ │ └── secrets.yaml
│ ├── overlays/ # Per-cluster overrides (Kustomize) │ ├── overlays/ # Per-cluster overrides
│ │ ├── upc-dev/ # UpCloud Dev (uses base as-is) │ │ ├── upc-dev/ # UpCloud Dev cluster (uses base as-is)
│ │ ── upc-prod/ # UpCloud Prod (patches value paths) │ │ ── upc-prod/ # UpCloud Prod cluster (patches value paths)
│ │ ├── eks-dev/ # AWS EKS Dev
│ │ ├── eks-prod/ # AWS EKS Prod
│ │ ├── aks-dev/ # Azure AKS Dev
│ │ ├── aks-prod/ # Azure AKS Prod
│ │ ├── gke-dev/ # GCP GKE Dev
│ │ └── gke-prod/ # GCP GKE Prod
│ ├── dashboards/ # Grafana dashboard ConfigMaps │ ├── dashboards/ # Grafana dashboard ConfigMaps
│ └── values/ # Helm value overrides │ └── values/ # Helm value overrides
│ ├── base/ # Shared cloud-agnostic values │ ├── base/ # Shared values (all clusters)
│ ├── upc-dev/ # UpCloud Dev (storage, LB, pricing) │ ├── upc-dev/ # UpCloud Dev-specific values
── upc-prod/ # UpCloud Prod ── upc-prod/ # UpCloud Prod-specific values
│ ├── eks-dev/ # AWS EKS Dev
│ ├── eks-prod/ # AWS EKS Prod
│ ├── aks-dev/ # Azure AKS Dev
│ ├── aks-prod/ # Azure AKS Prod
│ ├── gke-dev/ # GCP GKE Dev
│ └── gke-prod/ # GCP GKE Prod
├── apps/ # Business Applications ├── apps/ # Business Applications
│ ├── mcp10x.yaml │ ├── mcp10x.yaml
@@ -355,6 +343,7 @@ kubectl patch application myapp -n argocd \
| **Fluent-Bit** | Log shipping | `monitoring` | DaemonSet | | **Fluent-Bit** | Log shipping | `monitoring` | DaemonSet |
| **OpenCost** | Cost monitoring | `monitoring` | 1 | | **OpenCost** | Cost monitoring | `monitoring` | 1 |
| **Renovate** | Dependency updates | `renovate` | CronJob | | **Renovate** | Dependency updates | `renovate` | CronJob |
| **Trivy** | Vulnerability scanning | `trivy-system` | 1 |
**Full specs**: [Technical Reference - Infrastructure Components](docs/REFERENCE.md#infrastructure-components) **Full specs**: [Technical Reference - Infrastructure Components](docs/REFERENCE.md#infrastructure-components)
@@ -372,7 +361,7 @@ kubectl patch application myapp -n argocd \
## 📖 Key Concepts ## 📖 Key Concepts
### App-of-Apps Pattern ### App-of-Apps Pattern
`_app-of-apps-{cluster}.yaml` is the root Application that manages all other Applications in `infra/`. Kustomize overlays in `infra/overlays/{cluster}/` render the base Applications with per-cluster patches (e.g., swapping value file paths). Supported clusters: `upc-dev`, `upc-prod`, `eks-dev`, `eks-prod`, `aks-dev`, `aks-prod`, `gke-dev`, `gke-prod`. `_app-of-apps.yaml` is the root Application that manages all other Applications in `infra/`. Kustomize overlays in `infra/overlays/{upc-dev,upc-prod}/` render the base Applications with per-cluster patches (e.g., swapping value file paths from `upc-dev` to `upc-prod`).
### Multi-Source Pattern ### Multi-Source Pattern
Applications reference both: Applications reference both:
@@ -469,14 +458,16 @@ Documentation lives in `docs/`. To update:
## 📝 Notes ## 📝 Notes
### Current Environment ### Current Environment
- **Provider**: Multi-cloud (UpCloud, AWS EKS, Azure AKS, GCP GKE) - **Provider**: UpCloud Managed Kubernetes
- **Active clusters**: UpCloud (upc-dev, upc-prod)
- **Environment**: Production (internal use only) - **Environment**: Production (internal use only)
- **Clusters**: Multi-cluster (upc-dev, upc-prod) via Kustomize overlays
- **Auth**: Disabled for ArgoCD (internal access) - **Auth**: Disabled for ArgoCD (internal access)
- **Backup**: Gitea daily backup to S3-compatible storage - **Backup**: None (cluster rebuildable via GitOps)
### Known Limitations ### Known Limitations
- No automated backups (yet)
- Secret rotation not automated - Secret rotation not automated
- Multi-cluster limited to upc-dev and upc-prod environments
- DNS management is manual - DNS management is manual
**Future improvements**: See [Operations Runbook - Disaster Recovery](docs/OPERATIONS-RUNBOOK.md#disaster-recovery) **Future improvements**: See [Operations Runbook - Disaster Recovery](docs/OPERATIONS-RUNBOOK.md#disaster-recovery)
@@ -513,7 +504,7 @@ Internal use only. Not for public distribution.
--- ---
**Last Updated**: 2026-04-22 **Last Updated**: 2026-03-16
**Documentation Version**: 1.0.0 **Documentation Version**: 1.0.0
**🚀 Ready to get started? Check out the [Documentation Index](docs/README.md)!** **🚀 Ready to get started? Check out the [Documentation Index](docs/README.md)!**

View File

@@ -1,32 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: monitoring
annotations:
argocd.argoproj.io/sync-wave: "-1"
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infrastructure-apps
namespace: argocd
labels:
app.kubernetes.io/name: infrastructure-apps
app.kubernetes.io/part-of: platform
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD
path: infra/overlays/aks-dev
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

View File

@@ -1,32 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: monitoring
annotations:
argocd.argoproj.io/sync-wave: "-1"
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infrastructure-apps
namespace: argocd
labels:
app.kubernetes.io/name: infrastructure-apps
app.kubernetes.io/part-of: platform
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD
path: infra/overlays/aks-prod
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

View File

@@ -1,32 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: monitoring
annotations:
argocd.argoproj.io/sync-wave: "-1"
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infrastructure-apps
namespace: argocd
labels:
app.kubernetes.io/name: infrastructure-apps
app.kubernetes.io/part-of: platform
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD
path: infra/overlays/eks-dev
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

View File

@@ -1,32 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: monitoring
annotations:
argocd.argoproj.io/sync-wave: "-1"
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infrastructure-apps
namespace: argocd
labels:
app.kubernetes.io/name: infrastructure-apps
app.kubernetes.io/part-of: platform
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD
path: infra/overlays/eks-prod
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

View File

@@ -1,32 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: monitoring
annotations:
argocd.argoproj.io/sync-wave: "-1"
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infrastructure-apps
namespace: argocd
labels:
app.kubernetes.io/name: infrastructure-apps
app.kubernetes.io/part-of: platform
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD
path: infra/overlays/gke-prod
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

View File

@@ -18,7 +18,7 @@ metadata:
spec: spec:
project: default project: default
source: source:
repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git repoURL: git@github.com:fortedigital/sturdy-adventure.git
targetRevision: HEAD targetRevision: HEAD
path: infra/overlays/upc-prod path: infra/overlays/upc-prod
destination: destination:

View File

@@ -37,7 +37,7 @@ spec:
- $values/infra/values/base/dot-ai-stack-values.yaml - $values/infra/values/base/dot-ai-stack-values.yaml
- $values/infra/values/upc-dev/dot-ai-stack-values.yaml - $values/infra/values/upc-dev/dot-ai-stack-values.yaml
- repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git - repoURL: git@github.com:fortedigital/sturdy-adventure.git
targetRevision: HEAD targetRevision: HEAD
ref: values ref: values

View File

@@ -4,5 +4,4 @@ resources:
- dot-ai-stack.yaml - dot-ai-stack.yaml
- mcp10x.yaml - mcp10x.yaml
- musicman.yaml - musicman.yaml
- ts-mcp.yaml
- argo-mcp.yaml - argo-mcp.yaml

View File

@@ -1,40 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ts-mcp
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "11"
notifications.argoproj.io/subscribe.on-sync-succeeded.slack: ""
notifications.argoproj.io/subscribe.on-sync-failed.slack: ""
notifications.argoproj.io/subscribe.on-degraded.slack: ""
labels:
app.kubernetes.io/name: ts-mcp
app.kubernetes.io/part-of: apps
app.kubernetes.io/managed-by: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
sources:
- repoURL: ssh://git@git.forteapps.net:2222/Forte/forte-helm.git
path: forteapp
targetRevision: HEAD
helm:
valueFiles:
- $values/ts-mcp/values.yaml
- repoURL: ssh://git@git.forteapps.net:2222/Forte/helm-prod-values.git
targetRevision: HEAD
ref: values
destination:
server: https://kubernetes.default.svc
namespace: ts-mcp
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

View File

@@ -1,9 +1,8 @@
#!/bin/zsh #!/bin/zsh
# in case of $'\r': command not found error, run command below first # in case of $'\r': command not found error, run command below first
# sed -i 's/\r$//' ./bootstrap.sh # sed -i 's/\r$//' ./bootstrap.sh
CLUSTER="${1:?Usage: ./bootstrap.sh <cluster> (upc-dev|upc-prod|aks-dev|aks-prod|eks-dev|eks-prod|gke-dev|gke-prod)}" CLUSTER="${1:?Usage: ./bootstrap.sh <cluster> (upc-dev|upc-prod)}"
echo "running $0 for cluster: ${CLUSTER}..." echo "running $0 for cluster: ${CLUSTER}..."
@@ -28,8 +27,8 @@ Bootstrap()
Gitea() Gitea()
{ {
echo "Installing secret..." echo "Installing secret..."
kubectl apply -f "private/${CLUSTER}/gitea-repo-main.yaml" kubectl apply -f private/gitea-repo-main.yaml
kubectl apply -f "private/${CLUSTER}/main.key" kubectl apply -f private/main.key
} }
############################################################ ############################################################
@@ -37,15 +36,10 @@ Gitea()
############################################################ ############################################################
ArgoCd() ArgoCd()
{ {
# Pre-create ConfigMap for repo-server env (must exist before Helm upgrade)
kubectl create namespace argocd --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f cluster-resources/argocd-repo-server-config.yaml
# install argocd # install argocd
echo "Installing ArgoCD..." echo "Installing ArgoCD..."
helm upgrade --install argocd argo-cd \ helm upgrade --install argocd argo-cd \
--repo https://argoproj.github.io/argo-helm \ --repo https://argoproj.github.io/argo-helm \
--version "7.8.0" \
--namespace argocd --create-namespace \ --namespace argocd --create-namespace \
--values infra/values/base/argocd-values.yaml \ --values infra/values/base/argocd-values.yaml \
--values "infra/values/${CLUSTER}/argocd-values.yaml" \ --values "infra/values/${CLUSTER}/argocd-values.yaml" \
@@ -55,4 +49,4 @@ ArgoCd()
kubectl apply -f "_app-of-apps-${CLUSTER}.yaml" -n argocd kubectl apply -f "_app-of-apps-${CLUSTER}.yaml" -n argocd
} }
Bootstrap # Bootstrap

View File

@@ -1,83 +0,0 @@
# CronJob: syncs OIDC client secret from registrar-managed
# argocd-oidc-credentials into argocd-secret (oidc.clientSecret key).
# Runs every 2 min. No-ops if source secret doesn't exist yet
# (safe for fresh deploys before Keycloak is up).
apiVersion: v1
kind: ServiceAccount
metadata:
name: argocd-oidc-sync
namespace: argocd
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: argocd-oidc-sync
namespace: argocd
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["argocd-oidc-credentials", "argocd-secret"]
verbs: ["get", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: argocd-oidc-sync
namespace: argocd
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: argocd-oidc-sync
subjects:
- kind: ServiceAccount
name: argocd-oidc-sync
namespace: argocd
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: argocd-oidc-sync
namespace: argocd
spec:
schedule: "*/2 * * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
template:
spec:
serviceAccountName: argocd-oidc-sync
restartPolicy: Never
containers:
- name: sync
image: bitnami/kubectl:latest
command: ["/bin/sh", "-c"]
args:
- |
set -e
# Exit gracefully if source secret doesn't exist yet
if ! kubectl get secret argocd-oidc-credentials -n argocd >/dev/null 2>&1; then
echo "argocd-oidc-credentials not found — skipping (Keycloak not ready yet)"
exit 0
fi
# Read current OIDC client secret
NEW_SECRET=$(kubectl get secret argocd-oidc-credentials -n argocd \
-o jsonpath='{.data.client-secret}' | base64 -d)
# Read current value in argocd-secret (if any)
CURRENT=$(kubectl get secret argocd-secret -n argocd \
-o jsonpath='{.data.oidc\.clientSecret}' 2>/dev/null | base64 -d || echo "")
# Only patch if changed
if [ "$NEW_SECRET" = "$CURRENT" ]; then
echo "oidc.clientSecret already up to date"
exit 0
fi
kubectl patch secret argocd-secret -n argocd --type merge \
-p "{\"stringData\":{\"oidc.clientSecret\":\"${NEW_SECRET}\"}}"
echo "Patched argocd-secret with oidc.clientSecret"

View File

@@ -1,9 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-repo-server-config
namespace: argocd
data:
# Disable git submodule checkout - submodules (e.g. shared-prompts)
# are not needed for K8s manifest generation
ARGOCD_GIT_MODULES_ENABLED: "false"

View File

@@ -57,17 +57,17 @@ spec:
- sh - sh
- -c - -c
- | - |
mc alias set s3 "${S3_ENDPOINT}" "${AWS_ACCESS_KEY_ID}" "${AWS_SECRET_ACCESS_KEY}" mc alias set upcloud "${S3_ENDPOINT}" "${AWS_ACCESS_KEY_ID}" "${AWS_SECRET_ACCESS_KEY}"
TIMESTAMP=$(date +%Y%m%d-%H%M%S) TIMESTAMP=$(date +%Y%m%d-%H%M%S)
KEY="gitea-dump-${TIMESTAMP}.zip" KEY="gitea-dump-${TIMESTAMP}.zip"
echo "Uploading ${KEY}..." echo "Uploading ${KEY}..."
mc cp /backup/gitea-dump.zip "s3/${S3_BUCKET}/${KEY}" && \ mc cp /backup/gitea-dump.zip "upcloud/${S3_BUCKET}/${KEY}" && \
echo "Upload complete." echo "Upload complete."
# Prune backups older than 7 days # Prune backups older than 7 days
echo "Pruning backups older than 7 days..." echo "Pruning backups older than 7 days..."
mc rm --older-than 7d --force "s3/${S3_BUCKET}/" 2>&1 || true mc rm --older-than 7d --force "upcloud/${S3_BUCKET}/" 2>&1 || true
echo "Pruning complete." echo "Pruning complete."
envFrom: envFrom:
- secretRef: - secretRef:

View File

@@ -26,6 +26,7 @@ spec:
- monitoring - monitoring
- secrets - secrets
- kyverno - kyverno
- trivy-system
match: match:
any: any:
- resources: - resources:

View File

@@ -16,6 +16,7 @@ spec:
- resources: - resources:
namespaces: namespaces:
- kube-system - kube-system
- trivy-system
- monitoring - monitoring
- argocd - argocd
- cert-manager - cert-manager

View File

@@ -1,12 +0,0 @@
# Cluster config reference — values must match the corresponding overlay files.
# Read by bootstrap.sh at install time; NOT auto-propagated to ArgoCD value files.
clusterName: dev-aks # → infra/values/aks-dev/argocd-values.yaml (notifications.context.clusterName)
domain: example.com # → infra/values/base/gitea-values.yaml, renovate-values.yaml, keycloak-values.yaml (subdomains)
argocdDomain: argocd.example.com # → infra/values/aks-dev/argocd-values.yaml (global.domain)
grafanaDomain: grafana.example.com # → infra/values/aks-dev/grafana-values.yaml (ingress.hosts)
keycloakDomain: id.example.com # → infra/values/aks-dev/keycloak-values.yaml (ingress.hostname)
dotaiDomain: kubemcp.example.com # → infra/values/aks-dev/dot-ai-stack-values.yaml (dot-ai.ingress.host) — create if needed
dotaiUiDomain: kubemcpui.example.com # → infra/values/aks-dev/dot-ai-stack-values.yaml (dot-ai-ui.ingress.host) — create if needed
letsencryptEmail: admin@example.com # → cluster-resources/letsencrypt-issuer.yaml (spec.acme.email)
trustedIPs: "10.0.0.0/8,168.63.129.16/32" # → infra/values/aks-dev/traefik-values.yaml (ports.*.trustedIPs) — VNet CIDR + Azure health probe
cloudProvider: azure # → determines overlay directory and cloud-specific LB/storage annotations

View File

@@ -1,12 +0,0 @@
# Cluster config reference — values must match the corresponding overlay files.
# Read by bootstrap.sh at install time; NOT auto-propagated to ArgoCD value files.
clusterName: prod-aks # → infra/values/aks-prod/argocd-values.yaml (notifications.context.clusterName)
domain: example.com # → infra/values/base/gitea-values.yaml, renovate-values.yaml, keycloak-values.yaml (subdomains)
argocdDomain: argocd.example.com # → infra/values/aks-prod/argocd-values.yaml (global.domain)
grafanaDomain: grafana.example.com # → infra/values/aks-prod/grafana-values.yaml (ingress.hosts)
keycloakDomain: id.example.com # → infra/values/aks-prod/keycloak-values.yaml (ingress.hostname)
dotaiDomain: kubemcp.example.com # → infra/values/aks-prod/dot-ai-stack-values.yaml (dot-ai.ingress.host) — create if needed
dotaiUiDomain: kubemcpui.example.com # → infra/values/aks-prod/dot-ai-stack-values.yaml (dot-ai-ui.ingress.host) — create if needed
letsencryptEmail: admin@example.com # → cluster-resources/letsencrypt-issuer.yaml (spec.acme.email)
trustedIPs: "10.0.0.0/8,168.63.129.16/32" # → infra/values/aks-prod/traefik-values.yaml (ports.*.trustedIPs) — VNet CIDR + Azure health probe
cloudProvider: azure # → determines overlay directory and cloud-specific LB/storage annotations

View File

@@ -1,12 +0,0 @@
# Cluster config reference — values must match the corresponding overlay files.
# Read by bootstrap.sh at install time; NOT auto-propagated to ArgoCD value files.
clusterName: dev-eks # → infra/values/eks-dev/argocd-values.yaml (notifications.context.clusterName)
domain: example.com # → infra/values/base/gitea-values.yaml, renovate-values.yaml, keycloak-values.yaml (subdomains)
argocdDomain: argocd.example.com # → infra/values/eks-dev/argocd-values.yaml (global.domain)
grafanaDomain: grafana.example.com # → infra/values/eks-dev/grafana-values.yaml (ingress.hosts)
keycloakDomain: id.example.com # → infra/values/eks-dev/keycloak-values.yaml (ingress.hostname)
dotaiDomain: kubemcp.example.com # → infra/values/eks-dev/dot-ai-stack-values.yaml (dot-ai.ingress.host) — create if needed
dotaiUiDomain: kubemcpui.example.com # → infra/values/eks-dev/dot-ai-stack-values.yaml (dot-ai-ui.ingress.host) — create if needed
letsencryptEmail: admin@example.com # → cluster-resources/letsencrypt-issuer.yaml (spec.acme.email)
trustedIPs: "10.0.0.0/8" # → infra/values/eks-dev/traefik-values.yaml (ports.*.trustedIPs) — VPC CIDR
cloudProvider: eks # → determines overlay directory and cloud-specific LB/storage annotations

View File

@@ -1,12 +0,0 @@
# Cluster config reference — values must match the corresponding overlay files.
# Read by bootstrap.sh at install time; NOT auto-propagated to ArgoCD value files.
clusterName: prod-eks # → infra/values/eks-prod/argocd-values.yaml (notifications.context.clusterName)
domain: example.com # → infra/values/base/gitea-values.yaml, renovate-values.yaml, keycloak-values.yaml (subdomains)
argocdDomain: argocd.example.com # → infra/values/eks-prod/argocd-values.yaml (global.domain)
grafanaDomain: grafana.example.com # → infra/values/eks-prod/grafana-values.yaml (ingress.hosts)
keycloakDomain: id.example.com # → infra/values/eks-prod/keycloak-values.yaml (ingress.hostname)
dotaiDomain: kubemcp.example.com # → infra/values/eks-prod/dot-ai-stack-values.yaml (dot-ai.ingress.host) — create if needed
dotaiUiDomain: kubemcpui.example.com # → infra/values/eks-prod/dot-ai-stack-values.yaml (dot-ai-ui.ingress.host) — create if needed
letsencryptEmail: admin@example.com # → cluster-resources/letsencrypt-issuer.yaml (spec.acme.email)
trustedIPs: "10.0.0.0/8" # → infra/values/eks-prod/traefik-values.yaml (ports.*.trustedIPs) — VPC CIDR
cloudProvider: eks # → determines overlay directory and cloud-specific LB/storage annotations

View File

@@ -1,12 +0,0 @@
# Cluster config reference — values must match the corresponding overlay files.
# Read by bootstrap.sh at install time; NOT auto-propagated to ArgoCD value files.
clusterName: dev-gke # → infra/values/gke-dev/argocd-values.yaml (notifications.context.clusterName)
domain: example.com # → infra/values/base/gitea-values.yaml, renovate-values.yaml, keycloak-values.yaml (subdomains)
argocdDomain: argocd.example.com # → infra/values/gke-dev/argocd-values.yaml (global.domain)
grafanaDomain: grafana.example.com # → infra/values/gke-dev/grafana-values.yaml (ingress.hosts)
keycloakDomain: id.example.com # → infra/values/gke-dev/keycloak-values.yaml (ingress.hostname)
dotaiDomain: kubemcp.example.com # → infra/values/gke-dev/dot-ai-stack-values.yaml (dot-ai.ingress.host) — create if needed
dotaiUiDomain: kubemcpui.example.com # → infra/values/gke-dev/dot-ai-stack-values.yaml (dot-ai-ui.ingress.host) — create if needed
letsencryptEmail: admin@example.com # → cluster-resources/letsencrypt-issuer.yaml (spec.acme.email)
trustedIPs: "10.0.0.0/8,35.191.0.0/16,130.211.0.0/22" # → infra/values/gke-dev/traefik-values.yaml (ports.*.trustedIPs) — subnet + GCP health checks
cloudProvider: gke # → determines overlay directory and cloud-specific LB/storage annotations

View File

@@ -1,12 +0,0 @@
# Cluster config reference — values must match the corresponding overlay files.
# Read by bootstrap.sh at install time; NOT auto-propagated to ArgoCD value files.
clusterName: prod-gke # → infra/values/gke-prod/argocd-values.yaml (notifications.context.clusterName)
domain: example.com # → infra/values/base/gitea-values.yaml, renovate-values.yaml, keycloak-values.yaml (subdomains)
argocdDomain: argocd.example.com # → infra/values/gke-prod/argocd-values.yaml (global.domain)
grafanaDomain: grafana.example.com # → infra/values/gke-prod/grafana-values.yaml (ingress.hosts)
keycloakDomain: id.example.com # → infra/values/gke-prod/keycloak-values.yaml (ingress.hostname)
dotaiDomain: kubemcp.example.com # → infra/values/gke-prod/dot-ai-stack-values.yaml (dot-ai.ingress.host) — create if needed
dotaiUiDomain: kubemcpui.example.com # → infra/values/gke-prod/dot-ai-stack-values.yaml (dot-ai-ui.ingress.host) — create if needed
letsencryptEmail: admin@example.com # → cluster-resources/letsencrypt-issuer.yaml (spec.acme.email)
trustedIPs: "10.0.0.0/8,35.191.0.0/16,130.211.0.0/22" # → infra/values/gke-prod/traefik-values.yaml (ports.*.trustedIPs) — subnet + GCP health checks
cloudProvider: gke # → determines overlay directory and cloud-specific LB/storage annotations

View File

@@ -1,12 +1,10 @@
# Cluster config reference — values must match the corresponding overlay files. clusterName: dev-fd-no-svg1
# Read by bootstrap.sh at install time; NOT auto-propagated to ArgoCD value files. domain: forteapps.net
clusterName: dev-fd-no-svg1 # → infra/values/upc-dev/argocd-values.yaml (notifications.context.clusterName) argocdDomain: argocd.127.0.0.1.nip.io
domain: forteapps.net # → infra/values/base/gitea-values.yaml, renovate-values.yaml, keycloak-values.yaml (subdomains) grafanaDomain: grafana.forteapps.net
argocdDomain: argocd.127.0.0.1.nip.io # → infra/values/upc-dev/argocd-values.yaml (global.domain) keycloakDomain: id.forteapps.net
grafanaDomain: grafana.forteapps.net # → infra/values/upc-dev/grafana-values.yaml (ingress.hosts) dotaiDomain: kubemcp.forteapps.net
keycloakDomain: id.forteapps.net # → infra/values/upc-dev/keycloak-values.yaml (ingress.hostname) dotaiUiDomain: kubemcpui.forteapps.net
dotaiDomain: kubemcp.forteapps.net # → infra/values/upc-dev/dot-ai-stack-values.yaml (dot-ai.ingress.host) letsencryptEmail: danijels@gmail.com
dotaiUiDomain: kubemcpui.forteapps.net # → infra/values/upc-dev/dot-ai-stack-values.yaml (dot-ai-ui.ingress.host) trustedIPs: "172.16.1.0/24"
letsencryptEmail: danijels@gmail.com # → cluster-resources/letsencrypt-issuer.yaml (spec.acme.email) cloudProvider: upcloud
trustedIPs: "172.16.1.0/24" # → infra/values/upc-dev/traefik-values.yaml (ports.*.trustedIPs)
cloudProvider: upcloud # → determines overlay directory and cloud-specific LB/storage annotations

View File

@@ -1,12 +1,10 @@
# Cluster config reference — values must match the corresponding overlay files. clusterName: prod-fd-no-svg1
# Read by bootstrap.sh at install time; NOT auto-propagated to ArgoCD value files. domain: fortedigital.com
clusterName: prod-fd-no-svg1 # → infra/values/upc-prod/argocd-values.yaml (notifications.context.clusterName) argocdDomain: argocd.127.0.0.1.nip.io
domain: fortedigital.com # → infra/values/base/gitea-values.yaml, renovate-values.yaml, keycloak-values.yaml (subdomains) grafanaDomain: grafana.fortedigital.com
argocdDomain: argocd.127.0.0.1.nip.io # → infra/values/upc-prod/argocd-values.yaml (global.domain) keycloakDomain: id.fortedigital.com
grafanaDomain: grafana.fortedigital.com # → infra/values/upc-prod/grafana-values.yaml (ingress.hosts) dotaiDomain: kubemcp.fortedigital.com
keycloakDomain: id.fortedigital.com # → infra/values/upc-prod/keycloak-values.yaml (ingress.hostname) dotaiUiDomain: kubemcpui.fortedigital.com
dotaiDomain: kubemcp.fortedigital.com # → infra/values/upc-prod/dot-ai-stack-values.yaml (dot-ai.ingress.host) letsencryptEmail: danijel.simeunovic@fortedigital.com
dotaiUiDomain: kubemcpui.fortedigital.com # → infra/values/upc-prod/dot-ai-stack-values.yaml (dot-ai-ui.ingress.host) trustedIPs: "172.16.1.0/24"
letsencryptEmail: danijel.simeunovic@fortedigital.com # → cluster-resources/letsencrypt-issuer.yaml (spec.acme.email) cloudProvider: upcloud
trustedIPs: "172.16.1.0/24" # → infra/values/upc-prod/traefik-values.yaml (ports.*.trustedIPs)
cloudProvider: upcloud # → determines overlay directory and cloud-specific LB/storage annotations

View File

@@ -1,32 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.16.0/.schema/devbox.schema.json",
"packages": [
"kubectl@1.33.2",
"kubernetes-helm@3.18.4",
"k9s@0.50.7",
"kubeseal@0.30.0",
"argocd@2.14.11",
"kubecm@0.33.1",
"kubectl-tree@0.4.3",
"kind@0.29.0",
"kustomize@5.7.0",
"kyverno@1.14.3",
"syft@1.29.0",
"grype@0.92.2",
"traefik@3.6.7",
"claude-code@latest",
"go@latest",
"dotnet-sdk@latest",
"opentofu@1.11.6"
],
"shell": {
"init_hook": [
"echo 'Welcome to devbox!' > /dev/null"
],
"scripts": {
"test": [
"echo \"Error: no test specified\" && exit 1"
]
}
}
}

View File

@@ -654,11 +654,21 @@ kubectl create secret generic myapp-credentials \
#### Step 2: Seal the Secret #### Step 2: Seal the Secret
Get the public certificate (one-time setup):
```bash
# Fetch public cert from cluster
kubeseal --fetch-cert \
--controller-name=sealed-secrets-controller \
--controller-namespace=kube-system \
> pub-cert.pem
```
Seal your secret: Seal your secret:
```bash ```bash
kubeseal --format=yaml \ kubeseal --format=yaml \
--namespace=myapp \ --cert=pub-cert.pem \
< private/myapp-credentials.yaml \ < private/myapp-credentials.yaml \
> secrets/myapp-credentials-sealed.yaml > secrets/myapp-credentials-sealed.yaml
``` ```
@@ -701,7 +711,7 @@ kubectl create secret generic myapp-credentials \
# 2. Seal it # 2. Seal it
kubeseal --format=yaml \ kubeseal --format=yaml \
--namespace=myapp \ --cert=pub-cert.pem \
< private/myapp-credentials.yaml \ < private/myapp-credentials.yaml \
> secrets/myapp-credentials-sealed.yaml > secrets/myapp-credentials-sealed.yaml
@@ -952,46 +962,6 @@ User sees application (authenticated)
--- ---
### Accessing Authenticated User Information
The auth sidecar handles all authentication before requests reach your application. Your app never sees unauthenticated traffic — the sidecar returns 401 or redirects to the IdP first.
After successful authentication, the sidecar forwards the request to your application with user identity injected as HTTP headers:
| Header | Description | Available in |
|--------|-------------|-------------|
| `X-Auth-User` | Username or display name | Token, OIDC, MCP |
| `X-Auth-Email` | User email address | OIDC |
| `X-Auth-Subject` | OIDC `sub` claim (stable user ID) | OIDC, MCP |
| `X-Auth-Groups` | Comma-separated group memberships | OIDC (if scope includes `groups`) |
| `X-Auth-Token` | The validated access token | All modes |
**Your application reads these headers — no auth library needed:**
```javascript
// Express.js example
app.get('/profile', (req, res) => {
const user = req.headers['x-auth-user'];
const email = req.headers['x-auth-email'];
res.json({ user, email });
});
```
```python
# Flask example
@app.route('/profile')
def profile():
user = request.headers.get('X-Auth-User')
email = request.headers.get('X-Auth-Email')
return jsonify(user=user, email=email)
```
**Why this is safe**: The Kyverno-generated NetworkPolicy restricts ingress to the sidecar port only. Traffic cannot bypass the sidecar to reach the application port directly, so the `X-Auth-*` headers can be trusted unconditionally.
**Key principle**: Your application is zero-trust-unaware by design. It reads headers and renders UI. All authentication complexity lives in the sidecar and Kyverno policy.
---
### Authentication Configuration Reference ### Authentication Configuration Reference
#### Helm Values Schema #### Helm Values Schema

View File

@@ -12,11 +12,11 @@
## Overview ## Overview
This Kubernetes cluster uses a **GitOps approach** powered by **ArgoCD**, where Git repositories serve as the single source of truth for both infrastructure and application deployments. The cluster setup is **cloud-agnostic**, with ready-to-use configurations for **UpCloud**, **AWS EKS**, **Azure AKS**, and **GCP GKE**. This Kubernetes cluster uses a **GitOps approach** powered by **ArgoCD**, where Git repositories serve as the single source of truth for both infrastructure and application deployments. The cluster is running on **UpCloud Managed Kubernetes** but is designed to be cloud-agnostic.
### Key Characteristics ### Key Characteristics
- **Environment**: Production (internal use only) - **Environment**: Production (internal use only)
- **Cluster Type**: Multi-cloud, multi-cluster via Kustomize overlays (UpCloud, AWS, Azure, GCP) - **Cluster Type**: Multi-cluster (upc-dev, upc-prod) via Kustomize overlays
- **GitOps Tool**: ArgoCD - **GitOps Tool**: ArgoCD
- **Deployment Pattern**: App-of-Apps - **Deployment Pattern**: App-of-Apps
- **Secret Management**: Sealed Secrets (kubeseal) - **Secret Management**: Sealed Secrets (kubeseal)
@@ -63,7 +63,7 @@ This Kubernetes cluster uses a **GitOps approach** powered by **ArgoCD**, where
┌────────────────────────────────┐ ┌────────────────────────────────┐
│ Kubernetes Clusters │ │ Kubernetes Clusters │
│ (UpCloud, AWS, Azure, GCP) │ (UpCloud: upc-dev, upc-prod)
│ │ │ │
│ ┌──────────────────────────┐ │ │ ┌──────────────────────────┐ │
│ │ ArgoCD │ │ │ │ ArgoCD │ │
@@ -131,22 +131,26 @@ launchpad/
│ │ ├── renovate.yaml │ │ ├── renovate.yaml
│ │ ├── ... # All other Application manifests │ │ ├── ... # All other Application manifests
│ │ └── secrets.yaml │ │ └── secrets.yaml
│ ├── overlays/ # Per-cluster Kustomize overrides │ ├── overlays/ # Per-cluster overrides
│ │ ├── upc-dev/ # UpCloud Dev (uses base as-is) │ │ ├── upc-dev/ # UpCloud Dev (uses base as-is)
│ │ ── upc-prod/ # UpCloud Prod (patches value paths) │ │ ── upc-prod/ # UpCloud Prod (patches value paths)
│ │ ├── eks-dev/ # AWS EKS Dev
│ │ ├── eks-prod/ # AWS EKS Prod
│ │ ├── aks-dev/ # Azure AKS Dev
│ │ ├── aks-prod/ # Azure AKS Prod
│ │ ├── gke-dev/ # GCP GKE Dev
│ │ └── gke-prod/ # GCP GKE Prod
│ ├── dashboards/ # Grafana dashboard ConfigMaps │ ├── dashboards/ # Grafana dashboard ConfigMaps
│ └── values/ # Helm value overrides for infra │ └── values/ # Helm value overrides for infra
│ ├── base/ # Cloud-agnostic shared values │ ├── base/ # Shared values (all clusters)
├── upc-{dev,prod}/ # UpCloud: storage class, LB, pricing │ ├── traefik-values.yaml
├── aws-{dev,prod}/ # AWS: gp3, NLB, CUR pricing │ ├── keycloak-values.yaml
├── aks-{dev,prod}/ # Azure: managed-csi-premium, Standard LB │ ├── grafana-values.yaml
└── gcp-{dev,prod}/ # GCP: premium-rwo, L4 LB │ ├── prometheus-values.yaml
│ │ ├── gitea-values.yaml
│ │ └── ...
│ ├── upc-dev/ # upc-dev cluster-specific values
│ │ ├── traefik-values.yaml
│ │ ├── keycloak-values.yaml
│ │ └── grafana-values.yaml
│ └── upc-prod/ # upc-prod cluster-specific values
│ ├── traefik-values.yaml
│ ├── keycloak-values.yaml
│ └── grafana-values.yaml
├── apps/ # Business Application ArgoCD manifests (Kustomize) ├── apps/ # Business Application ArgoCD manifests (Kustomize)
│ ├── base/ # Base app manifests │ ├── base/ # Base app manifests
@@ -283,7 +287,7 @@ app-repository/
### The App-of-Apps Pattern ### The App-of-Apps Pattern
``` ```
_app-of-apps-{cluster}.yaml (Root, per cluster — e.g. upc-dev, eks-prod, gke-dev) _app-of-apps-{upc-dev,upc-prod}.yaml (Root, per cluster)
├── infrastructure-apps (manages infra/) ├── infrastructure-apps (manages infra/)
│ ├── cluster-resources-application │ ├── cluster-resources-application
@@ -373,15 +377,6 @@ patches:
value: $values/infra/values/upc-prod/traefik-values.yaml value: $values/infra/values/upc-prod/traefik-values.yaml
``` ```
Cloud-specific values (storage classes, load balancer annotations, cost model) are isolated in per-cluster value files. Base values are fully cloud-agnostic:
| Cloud | Storage Class | Load Balancer | OpenCost Provider |
|-------|--------------|---------------|-------------------|
| **UpCloud** | `upcloud-block-storage-maxiops` | UpCloud LB (ProxyProtocol v2) | Custom pricing |
| **AWS EKS** | `gp3` (EBS CSI) | NLB (ProxyProtocol v2) | AWS CUR |
| **Azure AKS** | `managed-csi-premium` | Standard LB (`externalTrafficPolicy: Local`) | Azure Billing API |
| **GCP GKE** | `premium-rwo` (PD CSI) | L4 passthrough NLB | GCP Cloud Billing |
**Benefits**: **Benefits**:
- Single source of truth for Application definitions - Single source of truth for Application definitions
- Cluster-specific values isolated per overlay - Cluster-specific values isolated per overlay
@@ -663,6 +658,6 @@ Notifications include:
--- ---
**Last Updated**: 2026-04-22 **Last Updated**: 2026-03-16
**Maintained By**: Platform Team **Maintained By**: Platform Team
**Questions?**: Contact #platform-support on Slack **Questions?**: Contact #platform-support on Slack

View File

@@ -37,7 +37,7 @@ Bootstrap a new cluster from scratch:
#### Prerequisites #### Prerequisites
1. **Kubernetes cluster running** (UpCloud, AWS EKS, Azure AKS, GCP GKE, or any K8s cluster) 1. **Kubernetes cluster running** (UpCloud or any K8s cluster)
2. **kubectl configured** with admin access 2. **kubectl configured** with admin access
3. **Repositories cloned** locally 3. **Repositories cloned** locally
@@ -54,13 +54,11 @@ kubectl get nodes
git clone https://git.forteapps.net/Forte/launchpad git clone https://git.forteapps.net/Forte/launchpad
cd launchpad cd launchpad
# 2. Run bootstrap script with cluster target # 2. Set cluster name (optional)
# Available clusters: upc-dev, upc-prod, eks-dev, eks-prod, export CLUSTER_NAME="prod-cluster-01"
# aks-dev, aks-prod, gke-dev, gke-prod
./bootstrap.sh upc-dev
# Cluster config is loaded from clusters/<cluster>.yaml # 3. Run bootstrap script
# (cloudProvider, trustedIPs, domain, etc.) ./bootstrap.sh
``` ```
**What Happens:** **What Happens:**
@@ -1264,21 +1262,13 @@ spec:
### Backup Strategy ### Backup Strategy
**Current State**: Gitea daily backups to S3-compatible storage **Current State**: No automated backups
**What Is Backed Up**: **What Needs Backup**:
- ✅ Gitea repositories + database: Daily CronJob (`cluster-resources/gitea-backup-cronjob.yaml`) uploads to S3-compatible storage with 7-day retention - ❌ Cluster state (not backed up - recreate via GitOps)
- ✅ Git repositories: Full cluster config recoverable from Git - ❌ Persistent volumes (currently not critical)
- ⚠️ Secrets: Sealed secrets in Git; unseal keys need safekeeping - ✅ Git repositories (Gitea provides backup)
- ⚠️ Secrets (sealed secrets in Git, unseal keys need safekeeping)
**What Is NOT Backed Up**:
- ❌ Cluster state (recreate via GitOps)
- ❌ Other persistent volumes (Prometheus, Loki, Tempo data)
**Per-cloud backup scripts** (manual restore helpers):
- UpCloud/AWS: `scripts/gitea-backup.sh` / `scripts/gitea-backup-eks.sh` (MinIO CLI, S3-compatible)
- Azure: `scripts/gitea-backup-aks.sh` (Azure CLI + Blob Storage)
- GCP: `scripts/gitea-backup-gke.sh` (gsutil + GCS)
### Cluster Rebuild ### Cluster Rebuild
@@ -1380,9 +1370,6 @@ kubectl get pods -n argocd
```bash ```bash
# UpCloud: Upgrade via control panel or CLI # UpCloud: Upgrade via control panel or CLI
# AWS EKS: eksctl upgrade cluster / AWS Console
# Azure AKS: az aks upgrade / Azure Portal
# GCP GKE: gcloud container clusters upgrade / Cloud Console
# After upgrade, verify cluster # After upgrade, verify cluster
kubectl version kubectl version
@@ -1520,35 +1507,18 @@ git push
### Multi-Cluster Setup ### Multi-Cluster Setup
The repository supports multiple clusters across multiple clouds via Kustomize overlays: The repository supports multiple clusters via Kustomize overlays:
**Active clusters:**
- **upc-dev** (default): `infra/overlays/upc-dev/` — uses base Applications as-is - **upc-dev** (default): `infra/overlays/upc-dev/` — uses base Applications as-is
- **upc-prod**: `infra/overlays/upc-prod/` — patches value file paths from `upc-dev` to `upc-prod` - **upc-prod**: `infra/overlays/upc-prod/` — patches value file paths from `upc-dev` to `upc-prod`
**Cloud-ready templates (fill in `clusters/*.yaml` before use):**
- **eks-dev** / **eks-prod**: AWS EKS with NLB, gp3 storage, AWS CUR pricing
- **aks-dev** / **aks-prod**: Azure AKS with Standard LB, managed-csi-premium storage
- **gke-dev** / **gke-prod**: GCP GKE with L4 LB, premium-rwo storage
Each cluster has its own: Each cluster has its own:
- Root app-of-apps: `_app-of-apps-{cluster}.yaml` - Root app-of-apps file: `_app-of-apps-upc-dev.yaml` / `_app-of-apps-upc-prod.yaml`
- Cluster config: `clusters/{cluster}.yaml` (domain, trustedIPs, cloudProvider) - Cluster-specific Helm values: `infra/values/upc-dev/` / `infra/values/upc-prod/`
- Kustomize overlay: `infra/overlays/{cluster}/kustomization.yaml` - Sealed secrets: `secrets/upc-dev/` (others as needed)
- Helm value overrides: `infra/values/{cluster}/` (traefik, gitea, opencost) - Apps overlay: `apps/overlays/upc-dev/` / `apps/overlays/upc-prod/`
- Sealed secrets: `secrets/{cluster}/` (as needed)
- Apps overlay: `apps/overlays/{cluster}/`
Cloud-specific values handled per-cluster: To add a new cluster, create a new overlay directory (e.g., `infra/overlays/upc-staging/`) with patches that swap the value file paths.
| Concern | UpCloud | AWS EKS | Azure AKS | GCP GKE |
|---------|---------|---------|-----------|---------|
| **Storage class** | `upcloud-block-storage-maxiops` | `gp3` | `managed-csi-premium` | `premium-rwo` |
| **Load balancer** | UpCloud LB + ProxyProtocol v2 | NLB + ProxyProtocol v2 | Standard LB + `externalTrafficPolicy: Local` | L4 passthrough NLB |
| **Cost monitoring** | Custom pricing | AWS CUR | Azure Billing API | GCP Cloud Billing |
| **Backup storage** | UpCloud S3-compat | AWS S3 (native) | Azure Blob Storage | GCS |
To add a new cluster, create a new overlay directory (e.g., `infra/overlays/eks-staging/`) with patches that swap the value file paths, and a matching `clusters/eks-staging.yaml`.
### Blue-Green Deployments ### Blue-Green Deployments
@@ -1691,6 +1661,6 @@ echo "Remember to delete: $SECRET_FILE"
--- ---
**Last Updated**: 2026-04-22 **Last Updated**: 2026-03-16
**Maintained By**: Platform Team **Maintained By**: Platform Team
**Emergency Contact**: #platform-support on Slack **Emergency Contact**: #platform-support on Slack

View File

@@ -180,7 +180,7 @@ Reference for:
┌──────────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────┐
│ Kubernetes Clusters (UpCloud, AWS, Azure, GCP) │ Kubernetes Clusters (UpCloud: upc-dev, upc-prod)
│ ┌──────────────────────────────────────────────────────┐ │ │ ┌──────────────────────────────────────────────────────┐ │
│ │ Infrastructure: Traefik, Cert-Manager, Kyverno │ │ │ │ Infrastructure: Traefik, Cert-Manager, Kyverno │ │
│ ├──────────────────────────────────────────────────────┤ │ │ ├──────────────────────────────────────────────────────┤ │
@@ -194,7 +194,7 @@ Reference for:
### Key Technologies ### Key Technologies
- **GitOps**: ArgoCD - **GitOps**: ArgoCD
- **Kubernetes**: Multi-cloud (UpCloud, AWS EKS, Azure AKS, GCP GKE) - **Kubernetes**: UpCloud Managed Kubernetes (multi-cluster: upc-dev, upc-prod)
- **Ingress**: Traefik v2 - **Ingress**: Traefik v2
- **Certificates**: Cert-Manager + Let's Encrypt - **Certificates**: Cert-Manager + Let's Encrypt
- **Policies**: Kyverno - **Policies**: Kyverno
@@ -299,16 +299,11 @@ docs/
## 🔄 Documentation Versions ## 🔄 Documentation Versions
**Current Version**: 1.0.0 **Current Version**: 1.0.0
**Last Updated**: 2026-04-22 **Last Updated**: 2026-03-16
**Maintained By**: Platform Team **Maintained By**: Platform Team
### Changelog ### Changelog
- **v1.1.0 (2026-04-22)**: Multi-cloud support
- Cloud-agnostic base values (storage, LB, pricing moved to per-cluster overlays)
- Added AWS EKS, Azure AKS, GCP GKE configurations
- Per-cloud backup scripts
- Updated all documentation
- **v1.0.0 (2026-03-16)**: Initial comprehensive documentation release - **v1.0.0 (2026-03-16)**: Initial comprehensive documentation release
- GitOps Architecture guide - GitOps Architecture guide
- Developer Onboarding guide - Developer Onboarding guide

View File

@@ -9,7 +9,6 @@
- [Kyverno Policies](#kyverno-policies) - [Kyverno Policies](#kyverno-policies)
- [Configuration Reference](#configuration-reference) - [Configuration Reference](#configuration-reference)
- [API Endpoints](#api-endpoints) - [API Endpoints](#api-endpoints)
- [Cloud Overlay Pattern](#cloud-overlay-pattern)
- [Glossary](#glossary) - [Glossary](#glossary)
--- ---
@@ -20,10 +19,9 @@
| Component | Value | | Component | Value |
|-----------|-------| |-----------|-------|
| **Provider** | Multi-cloud (UpCloud, AWS EKS, Azure AKS, GCP GKE) | | **Provider** | UpCloud Managed Kubernetes |
| **Environment** | Dev + Production per cloud | | **Environment** | Production (internal use) |
| **Active clusters** | UpCloud (upc-dev, upc-prod) | | **Cluster Count** | Multi-cluster (upc-dev, upc-prod) |
| **Cloud-ready templates** | EKS, AKS, GKE (dev + prod each) |
| **GitOps Tool** | ArgoCD | | **GitOps Tool** | ArgoCD |
| **Ingress Controller** | Traefik v2 | | **Ingress Controller** | Traefik v2 |
| **Certificate Management** | Cert-Manager + Let's Encrypt | | **Certificate Management** | Cert-Manager + Let's Encrypt |
@@ -44,7 +42,7 @@ Internet
[DNS: *.forteapps.net] [DNS: *.forteapps.net]
[Cloud Load Balancer] [UpCloud LoadBalancer]
[Traefik Ingress Controller] [Traefik Ingress Controller]
@@ -88,39 +86,22 @@ launchpad/
│ ├── loki.yaml │ ├── loki.yaml
│ ├── tempo.yaml │ ├── tempo.yaml
│ ├── fluent-bit.yaml │ ├── fluent-bit.yaml
│ ├── trivy.yaml
│ ├── gitea.yaml │ ├── gitea.yaml
│ ├── gitea-actions.yaml │ ├── gitea-actions.yaml
│ ├── sealedsecrets.yaml │ ├── sealedsecrets.yaml
│ ├── secrets.yaml │ ├── secrets.yaml
│ ├── renovate.yaml │ ├── renovate.yaml
│ ├── base/ # ArgoCD Application manifests (Kustomize base)
│ │ ├── gitea.yaml
│ │ ├── opencost.yaml
│ │ ├── traefik-application.yaml
│ │ ├── keycloak.yaml
│ │ ├── grafana.yaml
│ │ └── ...
│ ├── overlays/
│ │ └── upc-prod/
│ │ └── kustomization.yaml # Patches upc-dev → upc-prod valueFile paths
│ └── values/ │ └── values/
│ ├── base/ # Cloud-agnostic Helm values │ ├── argocd-values.yaml
│ ├── gitea-values.yaml ├── prometheus-values.yaml
│ ├── opencost-values.yaml ├── grafana-values.yaml
│ ├── prometheus-values.yaml ├── loki-values.yaml
│ └── ... ├── tempo-values.yaml
│ ├── upc-dev/ # UpCloud dev overlay values │ ├── gitea-values.yaml
│ ├── traefik-values.yaml ├── gitea-actions-values.yaml
│ ├── keycloak-values.yaml ├── fluent-bit-values.yaml
│ ├── grafana-values.yaml └── renovate-values.yaml
│ │ ├── gitea-values.yaml
│ │ └── opencost-values.yaml
│ └── upc-prod/ # UpCloud prod overlay values
│ ├── traefik-values.yaml
│ ├── keycloak-values.yaml
│ ├── grafana-values.yaml
│ ├── gitea-values.yaml
│ └── opencost-values.yaml
├── apps/ # Business applications ├── apps/ # Business applications
│ ├── mcp10x.yaml │ ├── mcp10x.yaml
@@ -147,39 +128,12 @@ launchpad/
│ └── auth-sidecar-injector.yaml │ └── auth-sidecar-injector.yaml
├── secrets/ # Application secrets (sealed) ├── secrets/ # Application secrets (sealed)
│ ├── base/ # All SealedSecrets (shared across clouds) │ ├── argocd-mcp-credentials.yaml
│ ├── kustomization.yaml │ ├── dot-ai-secrets.yaml
│ ├── argocd-forte-helm-secret-sealed.yaml │ ├── gitea-credentials-sealed.yaml
│ ├── argocd-mcp-credentials.yaml │ ├── gitea-runner-token-sealed.yaml
│ ├── argocdmcp-auth-oidc-sealed.yaml │ ├── mcp10x-credentials-sealed.yaml
│ ├── dot-ai-secrets.yaml └── musicman-credentials.yaml
│ │ ├── forte10x-app-credentials-sealed.yaml
│ │ ├── gitea-backup-s3-sealed.yaml
│ │ ├── gitea-credentials-sealed.yaml
│ │ ├── gitea-runner-token-sealed.yaml
│ │ ├── gitea-smtp-secret-sealed.yaml
│ │ ├── keycloak-credentials-sealed.yaml
│ │ ├── musicman-auth-oidc-sealed.yaml
│ │ ├── musicman-credentials.yaml
│ │ └── renovate-env-sealed.yaml
│ └── overlays/ # Per-cloud overlays (reference base)
│ ├── aks-dev/kustomization.yaml
│ ├── aks-prod/kustomization.yaml
│ ├── eks-dev/kustomization.yaml
│ ├── eks-prod/kustomization.yaml
│ ├── gke-dev/kustomization.yaml
│ ├── gke-prod/kustomization.yaml
│ ├── upc-dev/kustomization.yaml
│ └── upc-prod/kustomization.yaml
├── scripts/ # Operational helper scripts
│ ├── gitea-backup.sh # S3 backup helper (list/download)
│ ├── gitea-restore.sh
│ └── backup/ # Per-cloud backup reference scripts
│ ├── s3-minio.sh # S3-compatible (UpCloud, MinIO, Wasabi)
│ ├── aws-s3.sh # Native AWS S3
│ ├── azure-blob.sh # Azure Blob Storage
│ └── gcp-gcs.sh # GCP Cloud Storage
├── private/ # Local-only (Git-ignored) ├── private/ # Local-only (Git-ignored)
│ ├── *.yaml │ ├── *.yaml
@@ -648,95 +602,6 @@ retry:
4. 40 seconds 4. 40 seconds
5. 80 seconds (capped at 3 minutes) 5. 80 seconds (capped at 3 minutes)
### Global Settings (`argocd-cm`)
| Setting | Value | Purpose |
|---------|-------|---------|
| `application.resourceTrackingMethod` | `annotation` | Track resources via annotations |
| `timeout.reconciliation` | `60s` | Reconciliation interval |
| `admin.enabled` | `false` | Admin login disabled (SSO-only) |
| `url` | `https://argocd.forteapps.net` | External URL for ArgoCD UI |
**Git Submodule Disable**: Set via `configs.params` (NOT `repoServer.env` — that causes strategic merge conflicts with chart's `valueFrom` entries):
```yaml
configs:
params:
"reposerver.enable.git.submodule": "false"
```
This writes to `argocd-cmd-params-cm` ConfigMap, which the chart already reads via `valueFrom`. Submodules (e.g., `shared-prompts`) are not needed for K8s manifest generation.
**Break-Glass Admin Access**: Admin login is disabled (`admin.enabled: false`). The admin password remains in `argocd-secret`. To re-enable temporarily:
```bash
# Enable admin login
kubectl patch cm argocd-cm -n argocd -p '{"data":{"admin.enabled":"true"}}'
# Log in as admin, do what's needed, then disable again
kubectl patch cm argocd-cm -n argocd -p '{"data":{"admin.enabled":"false"}}'
```
ArgoCD picks up ConfigMap changes within the reconciliation timeout (60s). Note: ArgoCD will revert this on next sync — this is intentional (temporary access only).
**OIDC Authentication** (Keycloak):
```yaml
configs:
cm:
oidc.config: |
name: Forte SSO
issuer: https://id.forteapps.net/realms/forte
clientID: argocd
clientSecret: $oidc.clientSecret
requestedScopes: ["openid", "email", "profile"]
rbacConfig:
policy.csv: |
g, ArgoCD Admins, role:admin
g, ArgoCD Viewers, role:readonly
# Deny users not in any declared KC group
policy.default: ""
scopes: '[groups]'
```
**Access Control**: Only users in declared Keycloak groups can access ArgoCD. Users not in any group are denied (`policy.default: ""`). Assign users to groups in Keycloak admin console.
| KC Group | ArgoCD Role | Access |
|----------|-------------|--------|
| `ArgoCD Admins` | `role:admin` | Full control over all apps |
| `ArgoCD Viewers` | `role:readonly` | Read-only access to all apps |
| `Observability Team` | `role:observability` | Get/sync monitoring apps (prometheus, loki, fluent-bit, tempo, grafana, opencost) |
| `Dev Tools Team` | `role:devtools` | Get/sync dev tool apps (gitea, gitea-actions, renovate, karpor) |
| `App Developers` | `role:app-dev` | Get/sync/action on enterprise-apps only |
**Per-Cluster RBAC**: Add cluster-specific policies in `infra/values/<cluster>/argocd-values.yaml` using `configs.rbac.policy.<cluster>.csv`. ArgoCD concatenates all `policy.*.csv` keys alphabetically after `policy.csv`. Example:
```yaml
# infra/values/upc-dev/argocd-values.yaml
configs:
rbac:
policy.upc-dev.csv: |
p, role:staging-deployer, applications, sync, default/enterprise-apps, allow
g, Staging Deployers, role:staging-deployer
```
- ArgoCD does NOT add `openid` implicitly — must include in `requestedScopes`
- Do NOT add `groups` as a scope — the KC groups mapper emits the claim regardless
- `$oidc.clientSecret` references the `oidc.clientSecret` key in `argocd-secret`
- OIDC secret is synced by CronJob `argocd-oidc-sync` (see `cluster-resources/argocd-oidc-secret-sync.yaml`)
- The CronJob bridges `argocd-oidc-credentials` (from KC registrar) → `argocd-secret` every 2 min
- Safe for fresh deploys: no-ops if source secret doesn't exist yet
**Ingress** (Traefik + TLS):
```yaml
server:
ingress:
enabled: true
ingressClassName: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
tls: true
extraArgs:
- --insecure
configs:
params:
"server.insecure": true
```
TLS terminates at Traefik; ArgoCD runs in insecure mode behind the proxy.
--- ---
## Infrastructure Components ## Infrastructure Components
@@ -812,10 +677,6 @@ spec:
**Chart**: `sealed-secrets/sealed-secrets-controller` **Chart**: `sealed-secrets/sealed-secrets-controller`
**Namespace**: `kube-system` **Namespace**: `kube-system`
**Directory Structure**: `secrets/base/` contains all SealedSecrets with a `kustomization.yaml`. Per-cloud overlays in `secrets/overlays/<cloud>/` reference the base via Kustomize. The ArgoCD `secrets` Application points to the active overlay (e.g., `secrets/overlays/upc-dev`), and `infra/overlays/upc-prod` patches the path to `secrets/overlays/upc-prod`.
To add cloud-specific secrets, create a new SealedSecret in the overlay directory and add it to the overlay's `kustomization.yaml`.
**Public Certificate**: **Public Certificate**:
```bash ```bash
kubeseal --fetch-cert \ kubeseal --fetch-cert \
@@ -856,15 +717,6 @@ kubeStateMetrics:
- Loki - Loki
- Tempo - Tempo
**Ingress**: Exposed via Traefik at `https://grafana.forteapps.net` with cert-manager TLS.
**OIDC Authentication** (Keycloak):
- Uses `grafana.ini.auth.generic_oauth` with KC `grafana` client
- Secret `grafana-oidc-credentials` synced by KC registrar, loaded via `envFromSecrets`
- SSO-only mode: `auth.disable_login_form: true` + `auth.generic_oauth.auto_login: true`
- Role mapping via JMESPath on `resource_access.grafana.roles` claim (requires KC client role mapper)
- Roles: KC client roles `Admin`/`Editor` map to Grafana roles; default is `Viewer`
### Loki ### Loki
**Chart**: `grafana/loki-stack` **Chart**: `grafana/loki-stack`
@@ -967,8 +819,6 @@ postgresql:
**Email Notifications**: Enabled (`ENABLE_NOTIFY_MAIL: true`). SMTP credentials injected via `gitea-smtp-secret` using `additionalConfigFromEnvs` with `GITEA__mailer__USER` / `GITEA__mailer__PASSWD` environment variables. **Email Notifications**: Enabled (`ENABLE_NOTIFY_MAIL: true`). SMTP credentials injected via `gitea-smtp-secret` using `additionalConfigFromEnvs` with `GITEA__mailer__USER` / `GITEA__mailer__PASSWD` environment variables.
**Auto-Watch**: Disabled (`AUTO_WATCH_ON_CHANGES: false`, `AUTO_WATCH_NEW_REPOS: false`). Prevents contributors from being auto-subscribed to repo notifications on push, reducing email noise from CI bots (e.g., ai-review PR comments). Users who were already watching before this change need to manually unwatch or switch to "Only participating".
**Endpoints**: **Endpoints**:
- Web UI: `https://git.forteapps.net` - Web UI: `https://git.forteapps.net`
- SSH: port 22 (ClusterIP) - SSH: port 22 (ClusterIP)
@@ -1090,10 +940,10 @@ ignore:
| Secret | Purpose | | Secret | Purpose |
|--------|---------| |--------|---------|
| `ANTHROPIC_API_KEY` | Claude API key (from Anthropic console) | | `ANTHROPIC_API_KEY` | Claude API key (from Anthropic console) |
| `AI_REVIEW_TOKEN` | Gitea API token with `write:repository` + `read:repository` scopes (use a bot/service account) | | `AI_REVIEW_TOKEN` | Gitea API token with `write:issue` + `read:repository` scopes (use a bot/service account) |
**Setup Steps**: **Setup Steps**:
1. Create a Gitea bot/service account and generate an API token with `write:repository` + `read:repository` scopes 1. Create a Gitea bot/service account and generate an API token with `write:issue` + `read:repository` scopes
2. Add `AI_REVIEW_TOKEN` secret in Gitea repo settings → Actions → Secrets 2. Add `AI_REVIEW_TOKEN` secret in Gitea repo settings → Actions → Secrets
3. Add `ANTHROPIC_API_KEY` secret with your Anthropic API key 3. Add `ANTHROPIC_API_KEY` secret with your Anthropic API key
4. Ensure the `shared-prompts` submodule is initialized (`git submodule update --init`) 4. Ensure the `shared-prompts` submodule is initialized (`git submodule update --init`)
@@ -1217,33 +1067,6 @@ kubectl get secret keycloak-client-<app> -n keycloak -o jsonpath='{.metadata.ann
**See**: [Developer Guide - Adding a New Keycloak Client](DEVELOPER-GUIDE.md#adding-a-new-keycloak-client) **See**: [Developer Guide - Adding a New Keycloak Client](DEVELOPER-GUIDE.md#adding-a-new-keycloak-client)
### Karpor
**Chart**: `karpor` from `https://kusionstack.github.io/charts`
**Version**: 0.7.6 (app v0.6.4)
**Namespace**: `karpor`
**Sync Wave**: 1
**Purpose**: Kubernetes visualization and intelligence tool. Provides cross-cluster resource search, compliance checking, and topology visualization. Gives platform engineers a unified view of all cluster resources and their relationships.
**Architecture** (4 components):
- **Server** — main Karpor API/UI (port 7443)
- **Syncer** — syncs cluster state into the search index
- **ElasticSearch** — search backend for resource indexing
- **etcd** — persistent key-value store (10Gi PVC)
**Configuration** (`infra/values/base/karpor-values.yaml`):
- `namespaceEnabled: false` — ArgoCD manages namespace creation
- Default resource limits tuned for small clusters
- ElasticSearch: 2 CPU / 4Gi memory (the heaviest component)
- AI features available but not enabled (requires `server.ai.authToken` + backend config)
**Access**: Port-forward to reach the UI:
```bash
kubectl port-forward svc/karpor-release-server -n karpor 7443:7443
# Open https://localhost:7443
```
### Renovate ### Renovate
**Chart**: `renovate` (OCI: `ghcr.io/renovatebot/charts`) **Chart**: `renovate` (OCI: `ghcr.io/renovatebot/charts`)
@@ -1691,23 +1514,7 @@ Forward to Application (localhost:3000)
Application processes request Application processes request
``` ```
#### Forwarded Headers **See**: [Developer Guide - Enabling Authentication](DEVELOPER-GUIDE.md#enabling-authentication-for-applications) for usage examples.
After successful authentication, the sidecar injects user identity as HTTP headers before forwarding the request to the application container:
| Header | Description | Auth Modes |
|--------|-------------|------------|
| `X-Auth-User` | Username or display name | Token, OIDC, MCP |
| `X-Auth-Email` | User email address | OIDC |
| `X-Auth-Subject` | OIDC `sub` claim (stable user ID) | OIDC, MCP |
| `X-Auth-Groups` | Comma-separated group memberships | OIDC (if `groups` scope) |
| `X-Auth-Token` | The validated access token | All modes |
These headers are trustworthy because the auto-generated `NetworkPolicy` restricts pod ingress to the sidecar port only — external traffic cannot reach the application container directly, so headers cannot be spoofed.
Applications should read these headers to obtain authenticated user information (e.g. for display, authorisation decisions, or audit logging) instead of implementing their own authentication.
**See**: [Developer Guide - Accessing Authenticated User Information](DEVELOPER-GUIDE.md#accessing-authenticated-user-information) for code examples.
--- ---
@@ -1741,22 +1548,14 @@ Recommended resource allocation:
### Storage Classes ### Storage Classes
Storage classes are cloud-specific and configured in per-cluster value overrides (`infra/values/{cluster}/gitea-values.yaml`): Default storage class used: **UpCloud default** (varies by provider)
| Cloud | Storage Class | Driver |
|-------|--------------|--------|
| **UpCloud** | `upcloud-block-storage-maxiops` | UpCloud CSI |
| **AWS EKS** | `gp3` | EBS CSI |
| **Azure AKS** | `managed-csi-premium` | Azure Disk CSI |
| **GCP GKE** | `premium-rwo` | PD CSI |
```yaml ```yaml
# Example: base values omit storageClass (set in per-cluster overlay)
persistence: persistence:
enabled: true enabled: true
storageClass: "" # Uses default
accessMode: ReadWriteOnce accessMode: ReadWriteOnce
size: 5Gi size: 5Gi
# storageClass set by infra/values/{cluster}/gitea-values.yaml
``` ```
--- ---
@@ -1820,88 +1619,6 @@ POST /loki/api/v1/push
--- ---
## Cloud Overlay Pattern
### Overview
Cloud-specific configuration (StorageClass, LoadBalancer annotations, pricing models, etc.) lives in per-cloud overlay value files, **not** in `base/`. Adding a new cloud provider only requires a new overlay directory — no base changes.
### Supported Clouds
| Cloud | Dev overlay | Prod overlay | StorageClass | LB type |
|-------|-----------|-------------|-------------|---------|
| **UpCloud** | `upc-dev` | `upc-prod` | `upcloud-block-storage-maxiops` | UpCloud LB (proxy protocol v2) |
| **Azure AKS** | `aks-dev` | `aks-prod` | `managed-csi-premium` | Azure LB |
| **AWS EKS** | `eks-dev` | `eks-prod` | `gp3` | AWS NLB (proxy protocol) |
| **GCP GKE** | `gke-dev` | `gke-prod` | `premium-rwo` | GCP NEG |
Bootstrap any cluster with: `./bootstrap.sh <cluster>` (e.g., `./bootstrap.sh aks-dev`)
### How It Works
Each ArgoCD Application uses **multi-source Helm values** with two value files:
```yaml
# infra/base/gitea.yaml (example)
helm:
valueFiles:
- $values/infra/values/base/gitea-values.yaml # [0] cloud-agnostic
- $values/infra/values/upc-dev/gitea-values.yaml # [1] cloud-specific (default: upc-dev)
```
The `upc-prod` Kustomize overlay patches index `[1]` to swap the cloud-specific file:
```yaml
# infra/overlays/upc-prod/kustomization.yaml
- target:
kind: Application
name: gitea
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/upc-prod/gitea-values.yaml
```
### Components Using Cloud Overlays
| Component | Cloud-specific config | Overlay value file |
|-----------|----------------------|-------------------|
| **Traefik** | LB annotations, proxy protocol IPs | `traefik-values.yaml` |
| **Keycloak** | Hostname, TLS settings | `keycloak-values.yaml` |
| **Grafana** | Hostname, datasource URLs | `grafana-values.yaml` |
| **Gitea** | StorageClass (persistence + PostgreSQL) | `gitea-values.yaml` |
| **OpenCost** | Custom pricing model (CPU/RAM/storage rates) | `opencost-values.yaml` |
### Backup CronJob
The `gitea-backup` CronJob uses a generic `s3` alias for `minio/mc`. The actual endpoint and credentials come from the `gitea-backup-s3` Sealed Secret, which is per-cloud. Reference scripts for different cloud providers are in `scripts/backup/`:
| Script | Provider | Tool |
|--------|----------|------|
| `s3-minio.sh` | S3-compatible (UpCloud, MinIO, Wasabi) | `minio/mc` |
| `aws-s3.sh` | AWS S3 | `aws` CLI |
| `azure-blob.sh` | Azure Blob Storage | `az` CLI |
| `gcp-gcs.sh` | GCP Cloud Storage | `gsutil` |
### Adding a New Cloud Provider
To add support for a new cloud (e.g., `oci-dev` for Oracle Cloud):
1. **Cluster config**: `clusters/oci-dev.yaml` — clusterName, domain, trustedIPs, cloudProvider
2. **Overlay value files** in `infra/values/oci-dev/`:
- `traefik-values.yaml` — LB annotations, proxy protocol config
- `keycloak-values.yaml` — hostname
- `grafana-values.yaml` — hostname
- `gitea-values.yaml``storageClass` for persistence + PostgreSQL
- `opencost-values.yaml` — pricing model or cloud billing integration
3. **Kustomize overlay**: `infra/overlays/oci-dev/kustomization.yaml` — patch `valueFiles[1]` for each Application
4. **App-of-apps**: `_app-of-apps-oci-dev.yaml` — points to `infra/overlays/oci-dev`
5. **Secrets overlay**: `secrets/overlays/oci-dev/kustomization.yaml` — references `../../base`, add cloud-specific SealedSecrets if needed
6. **Secrets patch**: Add patch to `infra/overlays/oci-dev/kustomization.yaml` to swap secrets path to `secrets/overlays/oci-dev`
7. **Bootstrap**: `./bootstrap.sh oci-dev`
---
## Glossary ## Glossary
### Terms ### Terms
@@ -2034,6 +1751,6 @@ team: platform
--- ---
**Last Updated**: 2026-04-22 **Last Updated**: 2026-04-16
**Maintained By**: Platform Team **Maintained By**: Platform Team
**Version**: 1.0.0 **Version**: 1.0.0

View File

@@ -22,7 +22,6 @@ spec:
releaseName: gitea releaseName: gitea
valueFiles: valueFiles:
- $values/infra/values/base/gitea-values.yaml - $values/infra/values/base/gitea-values.yaml
- $values/infra/values/upc-dev/gitea-values.yaml
- repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git - repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD targetRevision: HEAD

View File

@@ -1,48 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: karpor
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "1"
labels:
app.kubernetes.io/name: karpor
app.kubernetes.io/part-of: developer-portal
app.kubernetes.io/managed-by: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
sources:
- repoURL: https://kusionstack.github.io/charts
chart: karpor
targetRevision: "0.7.6"
helm:
releaseName: karpor
valueFiles:
- $values/infra/values/base/karpor-values.yaml
- repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD
ref: values
destination:
server: https://kubernetes.default.svc
namespace: karpor
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- Validate=true
- ServerSideApply=true
ignoreDifferences:
- group: apps
kind: StatefulSet
jsonPointers:
- /spec/volumeClaimTemplates

View File

@@ -15,7 +15,7 @@ spec:
project: default project: default
sources: sources:
- repoURL: registry-1.docker.io/bitnamicharts - repoURL: https://charts.bitnami.com/bitnami
chart: keycloak chart: keycloak
targetRevision: "25.2.0" targetRevision: "25.2.0"
helm: helm:
@@ -47,7 +47,3 @@ spec:
kind: CronJob kind: CronJob
jsonPointers: jsonPointers:
- /spec/jobTemplate/spec/template/spec/containers/0/args - /spec/jobTemplate/spec/template/spec/containers/0/args
- group: apps
kind: StatefulSet
jsonPointers:
- /spec/volumeClaimTemplates

View File

@@ -10,6 +10,7 @@ resources:
- prometheus.yaml - prometheus.yaml
- loki.yaml - loki.yaml
- fluent-bit.yaml - fluent-bit.yaml
- trivy.yaml
- enterprise-apps.yaml - enterprise-apps.yaml
- cluster-resources-application.yaml - cluster-resources-application.yaml
- kyverno-policies.yaml - kyverno-policies.yaml
@@ -20,4 +21,4 @@ resources:
- renovate.yaml - renovate.yaml
- tempo.yaml - tempo.yaml
- grafana-dashboards.yaml - grafana-dashboards.yaml
- karpor.yaml - network-policies-application.yaml

View File

@@ -40,9 +40,3 @@ spec:
- CreateNamespace=true - CreateNamespace=true
- Validate=true - Validate=true
- ServerSideApply=true - ServerSideApply=true
ignoreDifferences:
- group: apps
kind: StatefulSet
jsonPointers:
- /spec/volumeClaimTemplates

View File

@@ -1,32 +1,33 @@
apiVersion: v1
kind: Namespace
metadata:
name: monitoring
annotations:
argocd.argoproj.io/sync-wave: "-1"
---
apiVersion: argoproj.io/v1alpha1 apiVersion: argoproj.io/v1alpha1
kind: Application kind: Application
metadata: metadata:
name: infrastructure-apps name: network-policies
namespace: argocd namespace: argocd
labels: labels:
app.kubernetes.io/name: infrastructure-apps app.kubernetes.io/name: network-policies
app.kubernetes.io/part-of: platform app.kubernetes.io/part-of: platform
app.kubernetes.io/managed-by: argocd
annotations:
argocd.argoproj.io/sync-wave: "1"
finalizers: finalizers:
- resources-finalizer.argocd.argoproj.io - resources-finalizer.argocd.argoproj.io
spec: spec:
project: default project: default
source: source:
repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD targetRevision: HEAD
path: infra/overlays/gke-dev path: cluster-resources/network
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: default
syncPolicy: syncPolicy:
automated: automated:
prune: true prune: true
selfHeal: true selfHeal: true
allowEmpty: false
syncOptions: syncOptions:
- CreateNamespace=true - Validate=true
- ServerSideApply=true

View File

@@ -22,9 +22,8 @@ spec:
releaseName: opencost releaseName: opencost
valueFiles: valueFiles:
- $values/infra/values/base/opencost-values.yaml - $values/infra/values/base/opencost-values.yaml
- $values/infra/values/upc-dev/opencost-values.yaml
- repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git - repoURL: git@github.com:fortedigital/sturdy-adventure.git
targetRevision: HEAD targetRevision: HEAD
ref: values ref: values

View File

@@ -18,7 +18,7 @@ spec:
project: default project: default
source: source:
repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
path: secrets/overlays/upc-dev path: secrets/upc-dev
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: secrets namespace: secrets

View File

@@ -40,9 +40,3 @@ spec:
- CreateNamespace=true - CreateNamespace=true
- Validate=true - Validate=true
- ServerSideApply=true - ServerSideApply=true
ignoreDifferences:
- group: apps
kind: StatefulSet
jsonPointers:
- /spec/volumeClaimTemplates

View File

@@ -31,7 +31,7 @@ spec:
- $values/infra/values/base/traefik-values.yaml - $values/infra/values/base/traefik-values.yaml
- $values/infra/values/upc-dev/traefik-values.yaml - $values/infra/values/upc-dev/traefik-values.yaml
- repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git - repoURL: git@github.com:fortedigital/sturdy-adventure.git
targetRevision: HEAD targetRevision: HEAD
ref: values ref: values

67
infra/base/trivy.yaml Normal file
View File

@@ -0,0 +1,67 @@
apiVersion: v1
kind: Namespace
metadata:
name: trivy-system
annotations:
argocd.argoproj.io/sync-wave: "-1"
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: trivy-operator
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "0"
labels:
app.kubernetes.io/name: trivy-operator
app.kubernetes.io/part-of: platform
app.kubernetes.io/managed-by: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://aquasecurity.github.io/helm-charts
chart: trivy-operator
targetRevision: 0.31.0
helm:
releaseName: trivy-operator
valuesObject:
operator:
targetNamespaces: ""
excludeNamespaces: "argocd,trivy-system,kube-system,monitoring,kyverno,cert-manager"
scanJobsInSameNamespace: true
metricsVulnIdEnabled: true
metricsImageInfo: true
trivy:
ignoreUnfixed: false
destination:
server: https://kubernetes.default.svc
namespace: trivy-system
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- Validate=true
- ServerSideApply=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
ignoreDifferences:
- group: apiextensions.k8s.io
kind: CustomResourceDefinition
jsonPointers:
- /metadata/labels
- /metadata/annotations
- /metadata/finalizers

View File

@@ -8,6 +8,9 @@ generatorOptions:
grafana_dashboard: "1" grafana_dashboard: "1"
configMapGenerator: configMapGenerator:
- name: grafana-dashboard-trivy
files:
- trivy.json
- name: grafana-dashboard-traefik-loki - name: grafana-dashboard-traefik-loki
files: files:
- traefik-loki.json - traefik-loki.json

1841
infra/dashboards/trivy.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,68 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
# Traefik: swap upc-dev → aks-dev
- target:
kind: Application
name: traefik
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/aks-dev/traefik-values.yaml
# Keycloak: swap upc-dev → aks-dev
- target:
kind: Application
name: keycloak
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/aks-dev/keycloak-values.yaml
# Grafana: swap upc-dev → aks-dev
- target:
kind: Application
name: grafana
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/aks-dev/grafana-values.yaml
# Gitea: swap upc-dev → aks-dev
- target:
kind: Application
name: gitea
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/aks-dev/gitea-values.yaml
# OpenCost: swap upc-dev → aks-dev
- target:
kind: Application
name: opencost
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/aks-dev/opencost-values.yaml
# Secrets: change path to aks-dev
- target:
kind: Application
name: secrets
patch: |
- op: replace
path: /spec/source/path
value: secrets/aks-dev
# Enterprise-apps: point to aks-dev overlay
- target:
kind: Application
name: enterprise-apps
patch: |
- op: replace
path: /spec/source/path
value: apps/overlays/aks-dev

View File

@@ -1,68 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
# Traefik: swap upc-dev → aks-prod
- target:
kind: Application
name: traefik
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/aks-prod/traefik-values.yaml
# Keycloak: swap upc-dev → aks-prod
- target:
kind: Application
name: keycloak
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/aks-prod/keycloak-values.yaml
# Grafana: swap upc-dev → aks-prod
- target:
kind: Application
name: grafana
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/aks-prod/grafana-values.yaml
# Gitea: swap upc-dev → aks-prod
- target:
kind: Application
name: gitea
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/aks-prod/gitea-values.yaml
# OpenCost: swap upc-dev → aks-prod
- target:
kind: Application
name: opencost
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/aks-prod/opencost-values.yaml
# Secrets: change path to aks-prod
- target:
kind: Application
name: secrets
patch: |
- op: replace
path: /spec/source/path
value: secrets/aks-prod
# Enterprise-apps: point to aks-prod overlay
- target:
kind: Application
name: enterprise-apps
patch: |
- op: replace
path: /spec/source/path
value: apps/overlays/aks-prod

View File

@@ -1,68 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
# Traefik: swap upc-dev → eks-dev
- target:
kind: Application
name: traefik
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/eks-dev/traefik-values.yaml
# Keycloak: swap upc-dev → eks-dev
- target:
kind: Application
name: keycloak
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/eks-dev/keycloak-values.yaml
# Grafana: swap upc-dev → eks-dev
- target:
kind: Application
name: grafana
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/eks-dev/grafana-values.yaml
# Gitea: swap upc-dev → eks-dev
- target:
kind: Application
name: gitea
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/eks-dev/gitea-values.yaml
# OpenCost: swap upc-dev → eks-dev
- target:
kind: Application
name: opencost
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/eks-dev/opencost-values.yaml
# Secrets: change path to eks-dev
- target:
kind: Application
name: secrets
patch: |
- op: replace
path: /spec/source/path
value: secrets/eks-dev
# Enterprise-apps: point to eks-dev overlay
- target:
kind: Application
name: enterprise-apps
patch: |
- op: replace
path: /spec/source/path
value: apps/overlays/eks-dev

View File

@@ -1,68 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
# Traefik: swap upc-dev → eks-prod
- target:
kind: Application
name: traefik
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/eks-prod/traefik-values.yaml
# Keycloak: swap upc-dev → eks-prod
- target:
kind: Application
name: keycloak
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/eks-prod/keycloak-values.yaml
# Grafana: swap upc-dev → eks-prod
- target:
kind: Application
name: grafana
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/eks-prod/grafana-values.yaml
# Gitea: swap upc-dev → eks-prod
- target:
kind: Application
name: gitea
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/eks-prod/gitea-values.yaml
# OpenCost: swap upc-dev → eks-prod
- target:
kind: Application
name: opencost
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/eks-prod/opencost-values.yaml
# Secrets: change path to eks-prod
- target:
kind: Application
name: secrets
patch: |
- op: replace
path: /spec/source/path
value: secrets/eks-prod
# Enterprise-apps: point to eks-prod overlay
- target:
kind: Application
name: enterprise-apps
patch: |
- op: replace
path: /spec/source/path
value: apps/overlays/eks-prod

View File

@@ -1,68 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
# Traefik: swap upc-dev → gke-dev
- target:
kind: Application
name: traefik
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/gke-dev/traefik-values.yaml
# Keycloak: swap upc-dev → gke-dev
- target:
kind: Application
name: keycloak
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/gke-dev/keycloak-values.yaml
# Grafana: swap upc-dev → gke-dev
- target:
kind: Application
name: grafana
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/gke-dev/grafana-values.yaml
# Gitea: swap upc-dev → gke-dev
- target:
kind: Application
name: gitea
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/gke-dev/gitea-values.yaml
# OpenCost: swap upc-dev → gke-dev
- target:
kind: Application
name: opencost
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/gke-dev/opencost-values.yaml
# Secrets: change path to gke-dev
- target:
kind: Application
name: secrets
patch: |
- op: replace
path: /spec/source/path
value: secrets/gke-dev
# Enterprise-apps: point to gke-dev overlay
- target:
kind: Application
name: enterprise-apps
patch: |
- op: replace
path: /spec/source/path
value: apps/overlays/gke-dev

View File

@@ -1,68 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
# Traefik: swap upc-dev → gke-prod
- target:
kind: Application
name: traefik
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/gke-prod/traefik-values.yaml
# Keycloak: swap upc-dev → gke-prod
- target:
kind: Application
name: keycloak
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/gke-prod/keycloak-values.yaml
# Grafana: swap upc-dev → gke-prod
- target:
kind: Application
name: grafana
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/gke-prod/grafana-values.yaml
# Gitea: swap upc-dev → gke-prod
- target:
kind: Application
name: gitea
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/gke-prod/gitea-values.yaml
# OpenCost: swap upc-dev → gke-prod
- target:
kind: Application
name: opencost
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/gke-prod/opencost-values.yaml
# Secrets: change path to gke-prod
- target:
kind: Application
name: secrets
patch: |
- op: replace
path: /spec/source/path
value: secrets/gke-prod
# Enterprise-apps: point to gke-prod overlay
- target:
kind: Application
name: enterprise-apps
patch: |
- op: replace
path: /spec/source/path
value: apps/overlays/gke-prod

View File

@@ -31,24 +31,6 @@ patches:
path: /spec/sources/0/helm/valueFiles/1 path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/upc-prod/grafana-values.yaml value: $values/infra/values/upc-prod/grafana-values.yaml
# Gitea: swap upc-dev → upc-prod
- target:
kind: Application
name: gitea
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/upc-prod/gitea-values.yaml
# OpenCost: swap upc-dev → upc-prod
- target:
kind: Application
name: opencost
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/upc-prod/opencost-values.yaml
# Secrets: change path to upc-prod # Secrets: change path to upc-prod
- target: - target:
kind: Application kind: Application
@@ -56,7 +38,7 @@ patches:
patch: | patch: |
- op: replace - op: replace
path: /spec/source/path path: /spec/source/path
value: secrets/overlays/upc-prod value: secrets/upc-prod
# Enterprise-apps: point to upc-prod overlay # Enterprise-apps: point to upc-prod overlay
- target: - target:

View File

@@ -1,7 +0,0 @@
# AKS-specific: Azure managed disk storage class
persistence:
storageClass: managed-csi-premium
postgresql:
primary:
persistence:
storageClass: managed-csi-premium

View File

@@ -1,4 +0,0 @@
# AKS-specific: Grafana hostname
ingress:
hosts:
- grafana.forteapps.net

View File

@@ -1,3 +0,0 @@
# AKS-specific: Keycloak hostname
ingress:
hostname: id.forteapps.net

View File

@@ -1,8 +0,0 @@
# AKS-specific: Azure pricing via Cloud Billing API
opencost:
exporter:
cloudProviderApiKey: ""
customPricing:
enabled: false
azure:
secretName: opencost-azure-billing

View File

@@ -1,11 +0,0 @@
# AKS-specific: Azure Load Balancer for Traefik
service:
annotations:
service.beta.kubernetes.io/azure-load-balancer-health-probe-request-path: /ping
ports:
web:
forwardedHeaders:
trustedIPs: "10.0.0.0/8"
websecure:
forwardedHeaders:
trustedIPs: "10.0.0.0/8"

View File

@@ -1,7 +0,0 @@
# AKS-specific: Azure managed disk storage class (prod)
persistence:
storageClass: managed-csi-premium
postgresql:
primary:
persistence:
storageClass: managed-csi-premium

View File

@@ -1,4 +0,0 @@
# AKS-specific: Grafana hostname (prod)
ingress:
hosts:
- grafana.fortedigital.com

View File

@@ -1,3 +0,0 @@
# AKS-specific: Keycloak hostname (prod)
ingress:
hostname: id.fortedigital.com

View File

@@ -1,8 +0,0 @@
# AKS-specific: Azure pricing via Cloud Billing API (prod)
opencost:
exporter:
cloudProviderApiKey: ""
customPricing:
enabled: false
azure:
secretName: opencost-azure-billing

View File

@@ -1,12 +0,0 @@
# AKS-specific: Azure Load Balancer for Traefik (prod)
service:
annotations:
service.beta.kubernetes.io/azure-load-balancer-health-probe-request-path: /ping
service.beta.kubernetes.io/azure-load-balancer-internal: "false"
ports:
web:
forwardedHeaders:
trustedIPs: "10.0.0.0/8"
websecure:
forwardedHeaders:
trustedIPs: "10.0.0.0/8"

View File

@@ -2,85 +2,16 @@ configs:
secret: secret:
createSecret: true createSecret: true
argocdServerAdminPassword: "$2b$12$Tmb1jH7ADvwWoUoNPXXsfOf6JqEluqhq8mL06a8DGT2AP1GzbNsCm" argocdServerAdminPassword: "$2b$12$Tmb1jH7ADvwWoUoNPXXsfOf6JqEluqhq8mL06a8DGT2AP1GzbNsCm"
# oidc.clientSecret managed by argocd-oidc-sync CronJob
# (reads from argocd-oidc-credentials, patches argocd-secret)
ssh:
knownHosts: |
[git.forteapps.net]:2222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDTwi40de8yTGUuRT0i/XGicQ672BLhYR6D/lDquJrp/tdrWoZhVVPy0wxSkWsq1V92iiAUuQnXagOGsLBGZT9uDLWKvEmNDnCfjzTMq3J1iA3vk2rQ8WBlCzhvmeCV/r0ufl6vsgfwxSRomLZeqa2UkLHx69gy2Njb1S2/aZK1Q53f466hCUfDULZrTn2Nn5Sj8cEbJ8EyvVN2YG9HYBxQdzKRPZEmS1vyzmn8YrYIkZseIRQElabzWGh86owuaaqnwJhTJj1j2sEUeIet04sGKJcnxx2UL4H90N66LKMldmMiuli+ve/CjJmMwDl0zGkjIniT3XR8CyEXYHli7B1hR8Z+dbK6DBgjz+28lFgMIRY70KkZJNsJcBNZLZ5fHwCI13a9U3Uhg3Pu/6s0zlosM4CrAQNQCRe95ZPtCpdFhlGrOl4m1rdSK2meL6rND0TBBuZbaFF6Py7TawLCAiO2KRaVqhu9OFVjwJ/nifgLzFGwWj+WcYmpuR+DwozrF/Hl7QYsz1x4GO1SONY07KbIFkUCHOMAh0AELY5YE4eGI4mtG6SecdPaAdLREGZYK4IcyP5i1QW9g0wmfRSsV9jy+r0ivBxixxh4yJiNpkg6NXak40gQtGIme9EJ+DxrRLruNsfDILWcdSuH/wvuorv56NpQFGB0FzB6LXMloSYptQ==
cm: cm:
application.resourceTrackingMethod: annotation application.resourceTrackingMethod: annotation
timeout.reconciliation: 60s timeout.reconciliation: 60s
# Admin login disabled — SSO only. Break-glass: kubectl patch cm argocd-cm -n argocd -p '{"data":{"admin.enabled":"true"}}' admin.enabled: "true"
admin.enabled: "false"
url: https://argocd.forteapps.net
oidc.config: |
name: Forte SSO
issuer: https://id.forteapps.net/realms/forte
clientID: argocd
clientSecret: $oidc.clientSecret
requestedScopes: ["openid", "email", "profile"]
rbac:
# Base RBAC — org-wide roles shared across all clusters.
# Per-cluster policies go in infra/values/<cluster>/argocd-values.yaml
# as configs.rbac.policy.<cluster>.csv (ArgoCD concatenates all policy.*.csv keys)
policy.csv: |
# Platform administrators — full control
g, ArgoCD Admins, role:admin
# Read-only viewers — see all, change nothing
g, ArgoCD Viewers, role:readonly
# --- Per-team roles (scoped to default project app names) ---
# Observability team — manage monitoring stack
p, role:observability, applications, get, default/prometheus, allow
p, role:observability, applications, get, default/loki, allow
p, role:observability, applications, get, default/fluent-bit, allow
p, role:observability, applications, get, default/tempo, allow
p, role:observability, applications, get, default/grafana, allow
p, role:observability, applications, get, default/grafana-dashboards, allow
p, role:observability, applications, get, default/opencost, allow
p, role:observability, applications, sync, default/prometheus, allow
p, role:observability, applications, sync, default/loki, allow
p, role:observability, applications, sync, default/fluent-bit, allow
p, role:observability, applications, sync, default/tempo, allow
p, role:observability, applications, sync, default/grafana, allow
p, role:observability, applications, sync, default/grafana-dashboards, allow
p, role:observability, applications, sync, default/opencost, allow
p, role:observability, logs, get, default/*, allow
g, Observability Team, role:observability
# Dev tools team — manage gitea, renovate, karpor
p, role:devtools, applications, get, default/gitea, allow
p, role:devtools, applications, get, default/gitea-actions, allow
p, role:devtools, applications, get, default/renovate, allow
p, role:devtools, applications, get, default/karpor, allow
p, role:devtools, applications, sync, default/gitea, allow
p, role:devtools, applications, sync, default/gitea-actions, allow
p, role:devtools, applications, sync, default/renovate, allow
p, role:devtools, applications, sync, default/karpor, allow
p, role:devtools, logs, get, default/*, allow
g, Dev Tools Team, role:devtools
# App developers — manage enterprise apps only
p, role:app-dev, applications, get, default/enterprise-apps, allow
p, role:app-dev, applications, sync, default/enterprise-apps, allow
p, role:app-dev, applications, action, default/enterprise-apps, allow
p, role:app-dev, logs, get, default/enterprise-apps, allow
g, App Developers, role:app-dev
# Deny users not in any declared KC group
policy.default: ""
scopes: '[groups]'
params: params:
"server.insecure": true "server.insecure": true
"reposerver.enable.git.submodule": "false"
server: server:
ingress: ingress:
enabled: true enabled: false
ingressClassName: traefik ingressClassName: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
tls: true
extraArgs: extraArgs:
- --insecure - --insecure

View File

@@ -29,10 +29,7 @@ gitea:
ALLOW_ONLY_EXTERNAL_REGISTRATION: true ALLOW_ONLY_EXTERNAL_REGISTRATION: true
ENABLE_BASIC_AUTHENTICATION: true ENABLE_BASIC_AUTHENTICATION: true
ENABLE_PASSWORD_SIGNIN_FORM: false ENABLE_PASSWORD_SIGNIN_FORM: false
AUTO_WATCH_ON_CHANGES: false ENABLE_NOTIFY_MAIL: true
AUTO_WATCH_NEW_REPOS: false
ENABLE_NOTIFY_MAIL: false
ENABLE_TIMETRACKING: false
openid: openid:
ENABLE_OPENID_SIGNIN: false ENABLE_OPENID_SIGNIN: false
@@ -77,7 +74,7 @@ gitea:
FROM: "noreply@fortedigital.com" FROM: "noreply@fortedigital.com"
admin: admin:
DEFAULT_EMAIL_NOTIFICATIONS: onmention DEFAULT_EMAIL_NOTIFICATIONS: enabled
# -- SMTP credentials injected from secret (USER and PASSWD) # -- SMTP credentials injected from secret (USER and PASSWD)
additionalConfigFromEnvs: additionalConfigFromEnvs:
@@ -98,7 +95,7 @@ gitea:
existingSecret: gitea-oidc-credentials existingSecret: gitea-oidc-credentials
key: gitea key: gitea
autoDiscoverUrl: "https://id.forteapps.net/realms/forte/.well-known/openid-configuration" autoDiscoverUrl: "https://id.forteapps.net/realms/forte/.well-known/openid-configuration"
scopes: "email profile organization" scopes: "openid email profile organization"
groupClaimName: "groups" groupClaimName: "groups"
adminGroup: "" adminGroup: ""
restrictedGroup: "" restrictedGroup: ""
@@ -130,6 +127,7 @@ persistence:
size: 10Gi size: 10Gi
accessModes: accessModes:
- ReadWriteOnce - ReadWriteOnce
storageClass: upcloud-block-storage-maxiops
# -- Recreate strategy to avoid Multi-Attach errors with RWO volumes # -- Recreate strategy to avoid Multi-Attach errors with RWO volumes
strategy: strategy:
@@ -155,6 +153,7 @@ postgresql:
persistence: persistence:
enabled: true enabled: true
size: 8Gi size: 8Gi
storageClass: upcloud-block-storage-maxiops
resources: resources:
requests: requests:
cpu: 100m cpu: 100m

View File

@@ -1,13 +1,5 @@
ingress: ingress:
enabled: true enabled: true
ingressClassName: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
tls:
- secretName: grafana-tls
hosts:
- grafana.forteapps.net
resources: resources:
requests: requests:
cpu: 50m cpu: 50m
@@ -19,29 +11,6 @@ resources:
adminUser: admin adminUser: admin
adminPassword: "forte" adminPassword: "forte"
envFromSecrets:
- name: grafana-oidc-credentials
grafana.ini:
server:
root_url: https://grafana.forteapps.net
auth.generic_oauth:
enabled: true
name: Forte SSO
allow_sign_up: true
client_id: ${client-id}
client_secret: ${client-secret}
scopes: openid email profile
auth_url: https://id.forteapps.net/realms/forte/protocol/openid-connect/auth
token_url: https://id.forteapps.net/realms/forte/protocol/openid-connect/token
api_url: https://id.forteapps.net/realms/forte/protocol/openid-connect/userinfo
role_attribute_path: "contains(resource_access.grafana.roles[*], 'Admin') && 'Admin' || contains(resource_access.grafana.roles[*], 'Editor') && 'Editor' || 'Viewer'"
role_attribute_strict: true
allow_assign_grafana_admin: true
auto_login: true
auth:
disable_login_form: true
datasources: datasources:
datasources.yaml: datasources.yaml:
apiVersion: 1 apiVersion: 1

View File

@@ -1,44 +0,0 @@
# Karpor - Kubernetes Visualization & Intelligence Tool
# Helm chart: https://github.com/KusionStack/charts/tree/master/charts/karpor
# Let the ArgoCD Application manage the namespace
namespaceEnabled: false
server:
replicas: 1
port: 7443
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 500m
memory: 1Gi
syncer:
replicas: 1
port: 7443
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 500m
memory: 1Gi
elasticsearch:
replicas: 1
port: 9200
resources:
requests:
cpu: 500m
memory: 2Gi
limits:
cpu: "2"
memory: 4Gi
etcd:
replicas: 1
port: 2379
persistence:
size: 5Gi

View File

@@ -75,6 +75,7 @@ keycloakConfigCli:
"publicClient": false, "publicClient": false,
"redirectUris": ["https://git.forteapps.net/*"], "redirectUris": ["https://git.forteapps.net/*"],
"webOrigins": ["https://git.forteapps.net"], "webOrigins": ["https://git.forteapps.net"],
"defaultClientScopes": ["openid", "email", "profile"],
"attributes": { "attributes": {
"k8s.secret.sync": "true", "k8s.secret.sync": "true",
"k8s.secret.namespace": "gitea", "k8s.secret.namespace": "gitea",
@@ -97,96 +98,6 @@ keycloakConfigCli:
} }
} }
] ]
},
{
"clientId": "grafana",
"name": "Grafana",
"enabled": true,
"protocol": "openid-connect",
"clientAuthenticatorType": "client-secret",
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"publicClient": false,
"redirectUris": ["https://grafana.forteapps.net/*"],
"webOrigins": ["https://grafana.forteapps.net"],
"attributes": {
"k8s.secret.sync": "true",
"k8s.secret.namespace": "monitoring",
"k8s.secret.name": "grafana-oidc-credentials",
"k8s.secret.client-id-key": "client-id",
"k8s.secret.client-secret-key": "client-secret"
},
"protocolMappers": [
{
"name": "client-roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-client-role-mapper",
"config": {
"claim.name": "resource_access.grafana.roles",
"jsonType.label": "String",
"multivalued": "true",
"usermodel.clientRoleMapping.clientId": "grafana",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
}
]
},
{
"clientId": "argocd",
"name": "ArgoCD",
"enabled": true,
"protocol": "openid-connect",
"clientAuthenticatorType": "client-secret",
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"publicClient": false,
"redirectUris": ["https://argocd.forteapps.net/auth/callback"],
"webOrigins": ["https://argocd.forteapps.net"],
"attributes": {
"k8s.secret.sync": "true",
"k8s.secret.namespace": "argocd",
"k8s.secret.name": "argocd-oidc-credentials",
"k8s.secret.client-id-key": "client-id",
"k8s.secret.client-secret-key": "client-secret"
},
"protocolMappers": [
{
"name": "groups",
"protocol": "openid-connect",
"protocolMapper": "oidc-group-membership-mapper",
"config": {
"claim.name": "groups",
"full.path": "false",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
}
]
}
],
"groups": [
{
"name": "ArgoCD Admins",
"path": "/ArgoCD Admins"
},
{
"name": "ArgoCD Viewers",
"path": "/ArgoCD Viewers"
},
{
"name": "Observability Team",
"path": "/Observability Team"
},
{
"name": "Dev Tools Team",
"path": "/Dev Tools Team"
},
{
"name": "App Developers",
"path": "/App Developers"
} }
] ]
} }
@@ -205,12 +116,12 @@ extraDeploy:
metadata: metadata:
name: keycloak-client-registrar name: keycloak-client-registrar
rules: rules:
- apiGroups: [ "" ] - apiGroups: [""]
resources: [ "secrets" ] resources: ["secrets"]
verbs: [ "get", "list", "create", "update", "patch" ] verbs: ["get", "list", "create", "update", "patch"]
- apiGroups: [ "" ] - apiGroups: [""]
resources: [ "namespaces" ] resources: ["namespaces"]
verbs: [ "get", "list" ] verbs: ["get", "list"]
# -- ClusterRoleBinding for the registrar ServiceAccount # -- ClusterRoleBinding for the registrar ServiceAccount
- apiVersion: rbac.authorization.k8s.io/v1 - apiVersion: rbac.authorization.k8s.io/v1
@@ -247,7 +158,7 @@ extraDeploy:
containers: containers:
- name: registrar - name: registrar
image: alpine:3.20 image: alpine:3.20
command: [ "/bin/sh", "-c" ] command: ["/bin/sh", "-c"]
args: args:
- | - |
set -e set -e

View File

@@ -10,8 +10,18 @@ opencost:
serviceName: prometheus-server serviceName: prometheus-server
namespaceName: monitoring namespaceName: monitoring
port: 80 port: 80
# Cloud-specific pricing is in per-cluster value overrides customPricing:
# (e.g. infra/values/upc-dev/opencost-values.yaml) enabled: true
provider: custom
costModel:
description: "UpCloud 4-node cluster pricing"
CPU: "5.86"
RAM: "1.46"
GPU: "0"
storage: "0.34"
zoneNetworkEgress: "0"
regionNetworkEgress: "0"
internetNetworkEgress: "0"
ui: ui:
enabled: false enabled: false
service: service:

View File

@@ -36,6 +36,28 @@ extraScrapeConfigs: |
- source_labels: [__meta_kubernetes_namespace] - source_labels: [__meta_kubernetes_namespace]
target_label: namespace target_label: namespace
- job_name: trivy-operator
scrape_interval: 30s
metrics_path: /metrics
kubernetes_sd_configs:
- role: pod
namespaces:
names:
- trivy-system
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name]
regex: trivy-operator
action: keep
- source_labels: [__meta_kubernetes_pod_container_port_number]
regex: "8080"
action: keep
- source_labels: [__meta_kubernetes_pod_name]
target_label: pod
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_instance]
target_label: instance
- job_name: traefik - job_name: traefik
scrape_interval: 15s scrape_interval: 15s
metrics_path: /metrics metrics_path: /metrics

View File

@@ -1,7 +0,0 @@
# EKS-specific: gp3 storage class
persistence:
storageClass: gp3
postgresql:
primary:
persistence:
storageClass: gp3

View File

@@ -1,4 +0,0 @@
# EKS-specific: Grafana hostname
ingress:
hosts:
- grafana.forteapps.net

View File

@@ -1,3 +0,0 @@
# EKS-specific: Keycloak hostname
ingress:
hostname: id.forteapps.net

View File

@@ -1,11 +0,0 @@
# EKS-specific: AWS pricing via Cost and Usage Report
opencost:
exporter:
cloudProviderApiKey: ""
customPricing:
enabled: false
aws:
spot_data_region: ""
spot_data_bucket: ""
spot_data_prefix: ""
account_id: ""

View File

@@ -1,17 +0,0 @@
# EKS-specific: AWS NLB for Traefik
service:
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: nlb
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"
ports:
web:
proxyProtocol:
trustedIPs: "10.0.0.0/8"
forwardedHeaders:
trustedIPs: "10.0.0.0/8"
websecure:
proxyProtocol:
trustedIPs: "10.0.0.0/8"
forwardedHeaders:
trustedIPs: "10.0.0.0/8"

View File

@@ -1,7 +0,0 @@
# EKS-specific: gp3 storage class (prod)
persistence:
storageClass: gp3
postgresql:
primary:
persistence:
storageClass: gp3

View File

@@ -1,4 +0,0 @@
# EKS-specific: Grafana hostname (prod)
ingress:
hosts:
- grafana.fortedigital.com

View File

@@ -1,3 +0,0 @@
# EKS-specific: Keycloak hostname (prod)
ingress:
hostname: id.fortedigital.com

View File

@@ -1,11 +0,0 @@
# EKS-specific: AWS pricing via Cost and Usage Report (prod)
opencost:
exporter:
cloudProviderApiKey: ""
customPricing:
enabled: false
aws:
spot_data_region: ""
spot_data_bucket: ""
spot_data_prefix: ""
account_id: ""

View File

@@ -1,18 +0,0 @@
# EKS-specific: AWS NLB for Traefik (prod)
service:
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: nlb
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"
service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
ports:
web:
proxyProtocol:
trustedIPs: "10.0.0.0/8"
forwardedHeaders:
trustedIPs: "10.0.0.0/8"
websecure:
proxyProtocol:
trustedIPs: "10.0.0.0/8"
forwardedHeaders:
trustedIPs: "10.0.0.0/8"

View File

@@ -1,7 +0,0 @@
# GKE-specific: SSD persistent disk storage class
persistence:
storageClass: premium-rwo
postgresql:
primary:
persistence:
storageClass: premium-rwo

View File

@@ -1,4 +0,0 @@
# GKE-specific: Grafana hostname
ingress:
hosts:
- grafana.forteapps.net

View File

@@ -1,3 +0,0 @@
# GKE-specific: Keycloak hostname
ingress:
hostname: id.forteapps.net

View File

@@ -1,10 +0,0 @@
# GKE-specific: GCP pricing via BigQuery billing export
opencost:
exporter:
cloudProviderApiKey: ""
customPricing:
enabled: false
google:
key: ""
project_id: ""
billing_account: ""

View File

@@ -1,12 +0,0 @@
# GKE-specific: Google Cloud Load Balancer for Traefik
service:
annotations:
cloud.google.com/neg: '{"ingress":true}'
networking.gke.io/load-balancer-type: External
ports:
web:
forwardedHeaders:
trustedIPs: "10.0.0.0/8"
websecure:
forwardedHeaders:
trustedIPs: "10.0.0.0/8"

View File

@@ -1,7 +0,0 @@
# GKE-specific: SSD persistent disk storage class (prod)
persistence:
storageClass: premium-rwo
postgresql:
primary:
persistence:
storageClass: premium-rwo

View File

@@ -1,4 +0,0 @@
# GKE-specific: Grafana hostname (prod)
ingress:
hosts:
- grafana.fortedigital.com

View File

@@ -1,3 +0,0 @@
# GKE-specific: Keycloak hostname (prod)
ingress:
hostname: id.fortedigital.com

View File

@@ -1,10 +0,0 @@
# GKE-specific: GCP pricing via BigQuery billing export (prod)
opencost:
exporter:
cloudProviderApiKey: ""
customPricing:
enabled: false
google:
key: ""
project_id: ""
billing_account: ""

View File

@@ -1,12 +0,0 @@
# GKE-specific: Google Cloud Load Balancer for Traefik (prod)
service:
annotations:
cloud.google.com/neg: '{"ingress":true}'
networking.gke.io/load-balancer-type: External
ports:
web:
forwardedHeaders:
trustedIPs: "10.0.0.0/8"
websecure:
forwardedHeaders:
trustedIPs: "10.0.0.0/8"

View File

@@ -1,5 +1,5 @@
global: global:
domain: argocd.forteapps.net domain: argocd.127.0.0.1.nip.io
notifications: notifications:
context: context:
clusterName: "dev-fd-eu-no-svg1" clusterName: "dev-fd-eu-no-svg1"

View File

@@ -1,7 +0,0 @@
# UpCloud storage class for Gitea and its embedded PostgreSQL
persistence:
storageClass: upcloud-block-storage-maxiops
postgresql:
primary:
persistence:
storageClass: upcloud-block-storage-maxiops

View File

@@ -1,15 +0,0 @@
# UpCloud custom pricing (no native OpenCost integration)
opencost:
exporter:
customPricing:
enabled: true
provider: custom
costModel:
description: "UpCloud 4-node cluster pricing"
CPU: "5.86"
RAM: "1.46"
GPU: "0"
storage: "0.34"
zoneNetworkEgress: "0"
regionNetworkEgress: "0"
internetNetworkEgress: "0"

View File

@@ -1,7 +0,0 @@
# UpCloud storage class for Gitea and its embedded PostgreSQL
persistence:
storageClass: upcloud-block-storage-maxiops
postgresql:
primary:
persistence:
storageClass: upcloud-block-storage-maxiops

View File

@@ -1,15 +0,0 @@
# UpCloud custom pricing (no native OpenCost integration)
opencost:
exporter:
customPricing:
enabled: true
provider: custom
costModel:
description: "UpCloud 4-node cluster pricing"
CPU: "5.86"
RAM: "1.46"
GPU: "0"
storage: "0.34"
zoneNetworkEgress: "0"
regionNetworkEgress: "0"
internetNetworkEgress: "0"

43
mkdocs.yml Normal file
View File

@@ -0,0 +1,43 @@
site_name: K8s Launchpad
site_description: Documentation for the GitOps-managed Kubernetes cluster
repo_url: https://git.forteapps.net/Forte/launchpad
repo_name: Forte/launchpad
theme:
name: material
palette:
- scheme: default
primary: indigo
toggle:
icon: material/brightness-7
name: Switch to dark mode
- scheme: slate
primary: indigo
toggle:
icon: material/brightness-4
name: Switch to light mode
features:
- navigation.instant
- navigation.sections
- navigation.top
- search.highlight
- content.code.copy
nav:
- Home: README.md
- GitOps Architecture: GITOPS-ARCHITECTURE.md
- Developer Guide: DEVELOPER-GUIDE.md
- Operations Runbook: OPERATIONS-RUNBOOK.md
- Technical Reference: REFERENCE.md
markdown_extensions:
- tables
- toc:
permalink: true
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
- admonition
- pymdownx.details

View File

@@ -1,23 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# AWS S3 backup upload (native AWS CLI)
# Uses: aws cli v2
# Env: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, S3_BUCKET
BACKUP_FILE="${1:?Usage: $0 <backup-file>}"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
KEY="gitea-dump-${TIMESTAMP}.zip"
echo "Uploading ${KEY}..."
aws s3 cp "$BACKUP_FILE" "s3://${S3_BUCKET}/${KEY}"
echo "Upload complete."
# Prune backups older than 7 days
echo "Pruning backups older than 7 days..."
CUTOFF=$(date -d '7 days ago' +%Y-%m-%dT%H:%M:%S 2>/dev/null || date -v-7d +%Y-%m-%dT%H:%M:%S)
aws s3api list-objects-v2 --bucket "${S3_BUCKET}" --query "Contents[?LastModified<'${CUTOFF}'].Key" --output text \
| tr '\t' '\n' \
| while read -r key; do
[ -n "$key" ] && aws s3 rm "s3://${S3_BUCKET}/${key}" && echo "Deleted: ${key}"
done
echo "Pruning complete."

View File

@@ -1,36 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Azure Blob Storage backup upload
# Uses: az cli
# Env: AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_KEY, AZURE_CONTAINER
BACKUP_FILE="${1:?Usage: $0 <backup-file>}"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
KEY="gitea-dump-${TIMESTAMP}.zip"
echo "Uploading ${KEY}..."
az storage blob upload \
--account-name "${AZURE_STORAGE_ACCOUNT}" \
--account-key "${AZURE_STORAGE_KEY}" \
--container-name "${AZURE_CONTAINER}" \
--name "${KEY}" \
--file "$BACKUP_FILE" \
--overwrite
echo "Upload complete."
# Prune backups older than 7 days
echo "Pruning backups older than 7 days..."
CUTOFF=$(date -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-7d +%Y-%m-%dT%H:%M:%SZ)
az storage blob list \
--account-name "${AZURE_STORAGE_ACCOUNT}" \
--account-key "${AZURE_STORAGE_KEY}" \
--container-name "${AZURE_CONTAINER}" \
--query "[?properties.lastModified<'${CUTOFF}'].name" -o tsv \
| while read -r name; do
[ -n "$name" ] && az storage blob delete \
--account-name "${AZURE_STORAGE_ACCOUNT}" \
--account-key "${AZURE_STORAGE_KEY}" \
--container-name "${AZURE_CONTAINER}" \
--name "$name" && echo "Deleted: ${name}"
done
echo "Pruning complete."

View File

@@ -1,26 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# GCP Cloud Storage backup upload
# Uses: gsutil (gcloud SDK)
# Env: GCS_BUCKET (e.g. gs://my-bucket)
BACKUP_FILE="${1:?Usage: $0 <backup-file>}"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
KEY="gitea-dump-${TIMESTAMP}.zip"
echo "Uploading ${KEY}..."
gsutil cp "$BACKUP_FILE" "${GCS_BUCKET}/${KEY}"
echo "Upload complete."
# Prune backups older than 7 days — GCS lifecycle rules are preferred,
# but this works as a manual fallback
echo "Pruning backups older than 7 days..."
CUTOFF=$(date -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-7d +%Y-%m-%dT%H:%M:%SZ)
gsutil ls -l "${GCS_BUCKET}/" \
| grep 'gitea-dump-' \
| while read -r size date name; do
if [[ "$date" < "$CUTOFF" ]]; then
gsutil rm "$name" && echo "Deleted: ${name}"
fi
done
echo "Pruning complete."

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# S3-compatible backup upload (UpCloud Objects, MinIO, Wasabi, etc.)
# Uses: minio/mc
# Env: S3_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_BUCKET
BACKUP_FILE="${1:?Usage: $0 <backup-file>}"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
KEY="gitea-dump-${TIMESTAMP}.zip"
mc alias set s3 "${S3_ENDPOINT}" "${AWS_ACCESS_KEY_ID}" "${AWS_SECRET_ACCESS_KEY}"
echo "Uploading ${KEY}..."
mc cp "$BACKUP_FILE" "s3/${S3_BUCKET}/${KEY}"
echo "Upload complete."
# Prune backups older than 7 days
echo "Pruning backups older than 7 days..."
mc rm --older-than 7d --force "s3/${S3_BUCKET}/" 2>&1 || true
echo "Pruning complete."

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