19 Commits

Author SHA1 Message Date
ecbb1f8638 pw 2026-04-23 23:00:44 +02:00
424be7ec7e allow login and sync 2026-04-23 22:49:53 +02:00
5afdf00964 session 2026-04-23 21:54:24 +02:00
2781c96d43 tls 2026-04-23 21:50:38 +02:00
a456a11460 db 2026-04-23 21:40:55 +02:00
b5e442d92b policy 2026-04-23 21:25:11 +02:00
2d756295bf backstage resources 2026-04-23 20:52:38 +02:00
026bcb2b31 feature/backstage (#13)
Reviewed-on: #13
Reviewed-by: gitea_admin <admin@forteapps.net>
Co-authored-by: Danijel Simeunovic <danijel.simeunovic@fortedigital.com>
Co-committed-by: Danijel Simeunovic <danijel.simeunovic@fortedigital.com>
2026-04-23 18:45:57 +00:00
aa6775bed2 ns 2026-04-23 14:52:27 +02:00
06522b2f19 ts-mcp 2026-04-23 14:44:33 +02:00
4c65035485 ns 2026-04-23 14:11:45 +02:00
84f4bebc08 ts-mcp 2026-04-23 13:41:51 +02:00
5394b2c714 ts-mcp 2026-04-23 13:40:33 +02:00
c4e586a7be ts-mcp 2026-04-23 13:38:47 +02:00
1fa070b041 argo 2026-04-23 13:35:42 +02:00
9c905355e3 argocd known host 2026-04-23 13:28:34 +02:00
6b1115ec28 argocd disable submodule 2026-04-23 13:09:02 +02:00
2fb276a62c ts-mcp 2026-04-23 13:02:00 +02:00
3efe1b68ef auth doc 2026-04-23 10:05:15 +02:00
65 changed files with 541 additions and 1198 deletions

View File

@@ -1,9 +1,9 @@
# 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/)
[![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
│ │ ├── ... # All other Application manifests
│ │ └── secrets.yaml
│ ├── overlays/ # Per-cluster overrides (Kustomize)
│ │ ├── upc-dev/ # UpCloud Dev (uses base as-is)
│ │ ── upc-prod/ # UpCloud Prod (patches value paths)
│ │ ├── aws-dev/ # AWS EKS Dev
│ │ ├── aws-prod/ # AWS EKS Prod
│ │ ├── azure-dev/ # Azure AKS Dev
│ │ ├── azure-prod/ # Azure AKS Prod
│ │ ├── gcp-dev/ # GCP GKE Dev
│ │ └── gcp-prod/ # GCP GKE Prod
│ ├── overlays/ # Per-cluster overrides
│ │ ├── upc-dev/ # UpCloud Dev cluster (uses base as-is)
│ │ ── upc-prod/ # UpCloud Prod cluster (patches value paths)
│ ├── dashboards/ # Grafana dashboard ConfigMaps
│ └── values/ # Helm value overrides
│ ├── base/ # Shared cloud-agnostic values
│ ├── upc-dev/ # UpCloud Dev (storage, LB, pricing)
── upc-prod/ # UpCloud Prod
│ ├── aws-dev/ # AWS EKS Dev
│ ├── aws-prod/ # AWS EKS Prod
│ ├── azure-dev/ # Azure AKS Dev
│ ├── azure-prod/ # Azure AKS Prod
│ ├── gcp-dev/ # GCP GKE Dev
│ └── gcp-prod/ # GCP GKE Prod
│ ├── base/ # Shared values (all clusters)
│ ├── upc-dev/ # UpCloud Dev-specific values
── upc-prod/ # UpCloud Prod-specific values
├── apps/ # Business Applications
│ ├── mcp10x.yaml
@@ -373,7 +361,7 @@ kubectl patch application myapp -n argocd \
## 📖 Key Concepts
### 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`, `aws-dev`, `aws-prod`, `azure-dev`, `azure-prod`, `gcp-dev`, `gcp-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
Applications reference both:
@@ -470,14 +458,16 @@ Documentation lives in `docs/`. To update:
## 📝 Notes
### Current Environment
- **Provider**: Multi-cloud (UpCloud, AWS EKS, Azure AKS, GCP GKE)
- **Active clusters**: UpCloud (upc-dev, upc-prod)
- **Provider**: UpCloud Managed Kubernetes
- **Environment**: Production (internal use only)
- **Clusters**: Multi-cluster (upc-dev, upc-prod) via Kustomize overlays
- **Auth**: Disabled for ArgoCD (internal access)
- **Backup**: Gitea daily backup to S3-compatible storage
- **Backup**: None (cluster rebuildable via GitOps)
### Known Limitations
- No automated backups (yet)
- Secret rotation not automated
- Multi-cluster limited to upc-dev and upc-prod environments
- DNS management is manual
**Future improvements**: See [Operations Runbook - Disaster Recovery](docs/OPERATIONS-RUNBOOK.md#disaster-recovery)
@@ -514,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
**🚀 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: git@github.com:fortedigital/sturdy-adventure.git
targetRevision: HEAD
path: infra/overlays/aws-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: git@github.com:fortedigital/sturdy-adventure.git
targetRevision: HEAD
path: infra/overlays/aws-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: git@github.com:fortedigital/sturdy-adventure.git
targetRevision: HEAD
path: infra/overlays/azure-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: git@github.com:fortedigital/sturdy-adventure.git
targetRevision: HEAD
path: infra/overlays/azure-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: git@github.com:fortedigital/sturdy-adventure.git
targetRevision: HEAD
path: infra/overlays/gcp-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: git@github.com:fortedigital/sturdy-adventure.git
targetRevision: HEAD
path: infra/overlays/gcp-prod
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

View File

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

50
apps/base/ts-mcp.yaml Normal file
View File

@@ -0,0 +1,50 @@
---
# Namespace must be created first (sync-wave: -1)
apiVersion: v1
kind: Namespace
metadata:
name: ts-mcp
annotations:
argocd.argoproj.io/sync-wave: "-1"
---
# ArgoCD Application syncs last (sync-wave: 11)
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

@@ -0,0 +1,43 @@
# Self-service Keycloak client config for Backstage.
# Kyverno clones this to the keycloak namespace, where the
# keycloak-client-registrar CronJob processes it and creates
# the backstage-oidc-credentials Secret in the backstage namespace.
apiVersion: v1
kind: Secret
metadata:
name: keycloak-client-backstage
namespace: backstage
labels:
keycloak.forteapps.net/client-config: "true"
stringData:
client.json: |
{
"clientId": "backstage",
"name": "Backstage Developer Portal",
"redirectUris": ["https://backstage.forteapps.net/api/auth/oidc/handler/frame"],
"webOrigins": ["https://backstage.forteapps.net"],
"defaultClientScopes": ["openid", "email", "profile"],
"protocolMappers": [
{
"name": "email_verified",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"config": {
"claim.name": "email_verified",
"claim.value": "true",
"jsonType.label": "boolean",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
}
],
"secret": {
"namespace": "backstage",
"name": "backstage-oidc-credentials",
"keys": {
"clientId": "AUTH_OIDC_CLIENT_ID",
"clientSecret": "AUTH_OIDC_CLIENT_SECRET"
}
}
}

View File

@@ -57,17 +57,17 @@ spec:
- sh
- -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)
KEY="gitea-dump-${TIMESTAMP}.zip"
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."
# 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
mc rm --older-than 7d --force "upcloud/${S3_BUCKET}/" 2>&1 || true
echo "Pruning complete."
envFrom:
- secretRef:

View File

@@ -1,41 +0,0 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-labels
annotations:
policies.kyverno.io/title: Require Labels
policies.kyverno.io/category: Best Practices
policies.kyverno.io/minversion: 1.6.0
policies.kyverno.io/severity: medium
policies.kyverno.io/subject: Pod, Label
policies.kyverno.io/description: Define and use labels that identify semantic attributes of your application or Deployment. A common set of labels allows tools to work collaboratively, describing objects in a common manner that all tools can understand. The recommended labels describe applications in a way that can be queried. This policy validates that the label `app.kubernetes.io/name` is specified with some value.
spec:
validationFailureAction: Audit
background: true
rules:
- name: check-for-labels
skipBackgroundRequests: true
exclude:
any:
- resources:
namespaces:
- kube-system
- istio-system
- argocd
- cert-manager
- monitoring
- secrets
- kyverno
- trivy-system
match:
any:
- resources:
kinds:
- Pod
validate:
message: The label `app.kubernetes.io/name` is required.
allowExistingViolations: true
pattern:
metadata:
labels:
app.kubernetes.io/name: "?*"

View File

@@ -1,10 +0,0 @@
clusterName: dev-eks # <- adjust to your EKS cluster name
domain: example.com # <- adjust to your domain
argocdDomain: argocd.example.com
grafanaDomain: grafana.example.com
keycloakDomain: id.example.com
dotaiDomain: kubemcp.example.com
dotaiUiDomain: kubemcpui.example.com
letsencryptEmail: admin@example.com # <- adjust
trustedIPs: "10.0.0.0/8" # <- adjust to your VPC CIDR
cloudProvider: aws

View File

@@ -1,10 +0,0 @@
clusterName: prod-eks # <- adjust to your EKS cluster name
domain: example.com # <- adjust to your domain
argocdDomain: argocd.example.com
grafanaDomain: grafana.example.com
keycloakDomain: id.example.com
dotaiDomain: kubemcp.example.com
dotaiUiDomain: kubemcpui.example.com
letsencryptEmail: admin@example.com # <- adjust
trustedIPs: "10.0.0.0/8" # <- adjust to your VPC CIDR
cloudProvider: aws

View File

@@ -1,10 +0,0 @@
clusterName: dev-aks # <- adjust to your AKS cluster name
domain: example.com # <- adjust to your domain
argocdDomain: argocd.example.com
grafanaDomain: grafana.example.com
keycloakDomain: id.example.com
dotaiDomain: kubemcp.example.com
dotaiUiDomain: kubemcpui.example.com
letsencryptEmail: admin@example.com # <- adjust
trustedIPs: "10.0.0.0/8,168.63.129.16/32" # <- VNet CIDR + Azure health probe
cloudProvider: azure

View File

@@ -1,10 +0,0 @@
clusterName: prod-aks # <- adjust to your AKS cluster name
domain: example.com # <- adjust to your domain
argocdDomain: argocd.example.com
grafanaDomain: grafana.example.com
keycloakDomain: id.example.com
dotaiDomain: kubemcp.example.com
dotaiUiDomain: kubemcpui.example.com
letsencryptEmail: admin@example.com # <- adjust
trustedIPs: "10.0.0.0/8,168.63.129.16/32" # <- VNet CIDR + Azure health probe
cloudProvider: azure

View File

@@ -1,10 +0,0 @@
clusterName: dev-gke # <- adjust to your GKE cluster name
domain: example.com # <- adjust to your domain
argocdDomain: argocd.example.com
grafanaDomain: grafana.example.com
keycloakDomain: id.example.com
dotaiDomain: kubemcp.example.com
dotaiUiDomain: kubemcpui.example.com
letsencryptEmail: admin@example.com # <- adjust
trustedIPs: "10.0.0.0/8,35.191.0.0/16,130.211.0.0/22" # <- subnet CIDR + GCP health checks
cloudProvider: gcp

View File

@@ -1,10 +0,0 @@
clusterName: prod-gke # <- adjust to your GKE cluster name
domain: example.com # <- adjust to your domain
argocdDomain: argocd.example.com
grafanaDomain: grafana.example.com
keycloakDomain: id.example.com
dotaiDomain: kubemcp.example.com
dotaiUiDomain: kubemcpui.example.com
letsencryptEmail: admin@example.com # <- adjust
trustedIPs: "10.0.0.0/8,35.191.0.0/16,130.211.0.0/22" # <- subnet CIDR + GCP health checks
cloudProvider: gcp

View File

@@ -962,6 +962,46 @@ 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
#### Helm Values Schema

View File

@@ -12,11 +12,11 @@
## 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
- **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
- **Deployment Pattern**: App-of-Apps
- **Secret Management**: Sealed Secrets (kubeseal)
@@ -63,7 +63,7 @@ This Kubernetes cluster uses a **GitOps approach** powered by **ArgoCD**, where
┌────────────────────────────────┐
│ Kubernetes Clusters │
│ (UpCloud, AWS, Azure, GCP)
│ (UpCloud: upc-dev, upc-prod)
│ │
│ ┌──────────────────────────┐ │
│ │ ArgoCD │ │
@@ -131,22 +131,26 @@ launchpad/
│ │ ├── renovate.yaml
│ │ ├── ... # All other Application manifests
│ │ └── secrets.yaml
│ ├── overlays/ # Per-cluster Kustomize overrides
│ ├── overlays/ # Per-cluster overrides
│ │ ├── upc-dev/ # UpCloud Dev (uses base as-is)
│ │ ── upc-prod/ # UpCloud Prod (patches value paths)
│ │ ├── aws-dev/ # AWS EKS Dev
│ │ ├── aws-prod/ # AWS EKS Prod
│ │ ├── azure-dev/ # Azure AKS Dev
│ │ ├── azure-prod/ # Azure AKS Prod
│ │ ├── gcp-dev/ # GCP GKE Dev
│ │ └── gcp-prod/ # GCP GKE Prod
│ │ ── upc-prod/ # UpCloud Prod (patches value paths)
│ ├── dashboards/ # Grafana dashboard ConfigMaps
│ └── values/ # Helm value overrides for infra
│ ├── base/ # Cloud-agnostic shared values
├── upc-{dev,prod}/ # UpCloud: storage class, LB, pricing
├── aws-{dev,prod}/ # AWS: gp3, NLB, CUR pricing
├── azure-{dev,prod}/ # Azure: managed-csi-premium, Standard LB
└── gcp-{dev,prod}/ # GCP: premium-rwo, L4 LB
│ ├── base/ # Shared values (all clusters)
│ ├── traefik-values.yaml
│ ├── keycloak-values.yaml
│ ├── grafana-values.yaml
│ ├── 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)
│ ├── base/ # Base app manifests
@@ -283,7 +287,7 @@ app-repository/
### The App-of-Apps Pattern
```
_app-of-apps-{cluster}.yaml (Root, per cluster — e.g. upc-dev, aws-prod, gcp-dev)
_app-of-apps-{upc-dev,upc-prod}.yaml (Root, per cluster)
├── infrastructure-apps (manages infra/)
│ ├── cluster-resources-application
@@ -373,15 +377,6 @@ patches:
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**:
- Single source of truth for Application definitions
- 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
**Questions?**: Contact #platform-support on Slack

View File

@@ -37,7 +37,7 @@ Bootstrap a new cluster from scratch:
#### 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
3. **Repositories cloned** locally
@@ -54,13 +54,11 @@ kubectl get nodes
git clone https://git.forteapps.net/Forte/launchpad
cd launchpad
# 2. Run bootstrap script with cluster target
# Available clusters: upc-dev, upc-prod, aws-dev, aws-prod,
# azure-dev, azure-prod, gcp-dev, gcp-prod
./bootstrap.sh upc-dev
# 2. Set cluster name (optional)
export CLUSTER_NAME="prod-cluster-01"
# Cluster config is loaded from clusters/<cluster>.yaml
# (cloudProvider, trustedIPs, domain, etc.)
# 3. Run bootstrap script
./bootstrap.sh
```
**What Happens:**
@@ -1264,21 +1262,13 @@ spec:
### Backup Strategy
**Current State**: Gitea daily backups to S3-compatible storage
**Current State**: No automated backups
**What Is Backed Up**:
- ✅ Gitea repositories + database: Daily CronJob (`cluster-resources/gitea-backup-cronjob.yaml`) uploads to S3-compatible storage with 7-day retention
- ✅ Git repositories: Full cluster config recoverable from Git
- ⚠️ 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-aws.sh` (MinIO CLI, S3-compatible)
- Azure: `scripts/gitea-backup-azure.sh` (Azure CLI + Blob Storage)
- GCP: `scripts/gitea-backup-gcp.sh` (gsutil + GCS)
**What Needs Backup**:
- ❌ Cluster state (not backed up - recreate via GitOps)
- ❌ Persistent volumes (currently not critical)
- ✅ Git repositories (Gitea provides backup)
- ⚠️ Secrets (sealed secrets in Git, unseal keys need safekeeping)
### Cluster Rebuild
@@ -1380,9 +1370,6 @@ kubectl get pods -n argocd
```bash
# 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
kubectl version
@@ -1520,35 +1507,18 @@ git push
### 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-prod**: `infra/overlays/upc-prod/` — patches value file paths from `upc-dev` to `upc-prod`
**Cloud-ready templates (fill in `clusters/*.yaml` before use):**
- **aws-dev** / **aws-prod**: AWS EKS with NLB, gp3 storage, AWS CUR pricing
- **azure-dev** / **azure-prod**: Azure AKS with Standard LB, managed-csi-premium storage
- **gcp-dev** / **gcp-prod**: GCP GKE with L4 LB, premium-rwo storage
Each cluster has its own:
- Root app-of-apps: `_app-of-apps-{cluster}.yaml`
- Cluster config: `clusters/{cluster}.yaml` (domain, trustedIPs, cloudProvider)
- Kustomize overlay: `infra/overlays/{cluster}/kustomization.yaml`
- Helm value overrides: `infra/values/{cluster}/` (traefik, gitea, opencost)
- Sealed secrets: `secrets/{cluster}/` (as needed)
- Apps overlay: `apps/overlays/{cluster}/`
- Root app-of-apps file: `_app-of-apps-upc-dev.yaml` / `_app-of-apps-upc-prod.yaml`
- Cluster-specific Helm values: `infra/values/upc-dev/` / `infra/values/upc-prod/`
- Sealed secrets: `secrets/upc-dev/` (others as needed)
- Apps overlay: `apps/overlays/upc-dev/` / `apps/overlays/upc-prod/`
Cloud-specific values handled per-cluster:
| 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/aws-staging/`) with patches that swap the value file paths, and a matching `clusters/aws-staging.yaml`.
To add a new cluster, create a new overlay directory (e.g., `infra/overlays/upc-staging/`) with patches that swap the value file paths.
### 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
**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 │ │
│ ├──────────────────────────────────────────────────────┤ │
@@ -194,7 +194,7 @@ Reference for:
### Key Technologies
- **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
- **Certificates**: Cert-Manager + Let's Encrypt
- **Policies**: Kyverno
@@ -299,16 +299,11 @@ docs/
## 🔄 Documentation Versions
**Current Version**: 1.0.0
**Last Updated**: 2026-04-22
**Last Updated**: 2026-03-16
**Maintained By**: Platform Team
### 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
- GitOps Architecture guide
- Developer Onboarding guide

View File

@@ -19,9 +19,9 @@
| Component | Value |
|-----------|-------|
| **Provider** | Multi-cloud (UpCloud, AWS EKS, Azure AKS, GCP GKE) |
| **Active clusters** | UpCloud (upc-dev, upc-prod) |
| **Cloud-ready templates** | AWS, Azure, GCP (dev + prod each) |
| **Provider** | UpCloud Managed Kubernetes |
| **Environment** | Production (internal use) |
| **Cluster Count** | Multi-cluster (upc-dev, upc-prod) |
| **GitOps Tool** | ArgoCD |
| **Ingress Controller** | Traefik v2 |
| **Certificate Management** | Cert-Manager + Let's Encrypt |
@@ -42,7 +42,7 @@ Internet
[DNS: *.forteapps.net]
[Cloud Load Balancer]
[UpCloud LoadBalancer]
[Traefik Ingress Controller]
@@ -602,6 +602,15 @@ retry:
4. 40 seconds
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` | `true` | Enable admin account |
| `git.submodule.enabled` | `false` | Disable git submodule checkout — submodules are not needed for manifest generation |
---
## Infrastructure Components
@@ -956,6 +965,83 @@ ignore:
- Check Gitea Actions tab for workflow run status and logs
- Monitor Anthropic usage dashboard for token consumption
### Backstage / RHDH (Developer Portal)
**Chart**: `backstage` (RHDH — Red Hat Developer Hub)
**Version**: `5.8.0`
**Namespace**: `backstage`
**Helm Repo**: `https://redhat-developer.github.io/rhdh-chart`
**Image**: `quay.io/rhdh-community/rhdh:next`
**Purpose**: Internal developer portal where teams register and broadcast themselves, their applications, APIs, and systems. Provides a unified catalog, templates, and documentation hub.
**Why RHDH over vanilla Backstage**: Ships 27+ plugins pre-bundled (ArgoCD, Kubernetes, Keycloak, GitHub, GitLab, Jira, SonarQube, Tekton, Jenkins, Quay, and more). Supports dynamic plugin installation at runtime — no image rebuilds needed.
**Configuration** (`infra/values/base/backstage-values.yaml`):
- OpenShift Route disabled (`route.enabled: false`) — uses Traefik ingress instead
- PostgreSQL subchart enabled for persistence (2Gi)
- SecurityContext configured for vanilla Kubernetes (non-OpenShift)
- Traefik ingress with `websecure` entrypoint
- App title: "Forte Developer Portal"
- Dynamic plugins: loads `dynamic-plugins.default.yaml` (all 27+ bundled plugins)
- Catalog rules: Component, System, API, Resource, Location, Template, Group, User, Domain
**Authentication** (Keycloak OIDC):
- Uses the self-service registrar pattern (see [Keycloak Client Registrar](#keycloak-client-registrar))
- Config Secret: `cluster-resources/backstage-keycloak-client-config.yaml`
- Kyverno clones it → registrar creates `backstage-oidc-credentials` Secret in `backstage` namespace
- Credential keys: `AUTH_OIDC_CLIENT_ID`, `AUTH_OIDC_CLIENT_SECRET` (loaded via `extraEnvVarsSecrets`)
- Redirect URI: `https://backstage.forteapps.net/api/auth/oidc/handler/frame`
- Sign-in resolver: `emailMatchingUserEntityProfileEmail`
**Catalog Discovery** (Gitea):
- Auto-discovers `catalog-info.yaml` from all repos in the `Forte` organization
- Scans every 30 minutes via the Gitea catalog provider plugin
- Gitea SCM integration configured for URL resolution (`git.forteapps.net`)
**Catalog Registration**:
Teams register services by adding a `catalog-info.yaml` to their repo root:
```yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: my-service
description: My service description
annotations:
backstage.io/source-location: url:https://git.forteapps.net/Forte/my-service
spec:
type: service
lifecycle: production
owner: team-name
```
Repos with this file are auto-discovered — no manual registration needed.
**Dynamic Plugins**:
Add plugins at runtime via `global.dynamic.plugins` in values — no image rebuild:
```yaml
global:
dynamic:
plugins:
- package: "@scope/my-plugin@1.0.0"
integrity: "sha512-..."
```
**Per-cluster Configuration** (`infra/values/upc-dev/backstage-values.yaml`):
```yaml
global:
host: backstage.forteapps.net
upstream:
backstage:
appConfig:
app:
baseUrl: https://backstage.forteapps.net
backend:
baseUrl: https://backstage.forteapps.net
ingress:
host: backstage.forteapps.net
```
### Keycloak Client Registrar
**Type**: CronJob (deployed via Keycloak Helm chart `extraDeploy`)
@@ -1516,7 +1602,23 @@ Forward to Application (localhost:3000)
Application processes request
```
**See**: [Developer Guide - Enabling Authentication](DEVELOPER-GUIDE.md#enabling-authentication-for-applications) for usage examples.
#### Forwarded Headers
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.
---
@@ -1550,22 +1652,14 @@ Recommended resource allocation:
### Storage Classes
Storage classes are cloud-specific and configured in per-cluster value overrides (`infra/values/{cluster}/gitea-values.yaml`):
| 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 |
Default storage class used: **UpCloud default** (varies by provider)
```yaml
# Example: base values omit storageClass (set in per-cluster overlay)
persistence:
enabled: true
storageClass: "" # Uses default
accessMode: ReadWriteOnce
size: 5Gi
# storageClass set by infra/values/{cluster}/gitea-values.yaml
```
---
@@ -1761,6 +1855,6 @@ team: platform
---
**Last Updated**: 2026-04-22
**Last Updated**: 2026-04-16
**Maintained By**: Platform Team
**Version**: 1.0.0

43
infra/base/backstage.yaml Normal file
View File

@@ -0,0 +1,43 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: backstage
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "1"
labels:
app.kubernetes.io/name: backstage
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://redhat-developer.github.io/rhdh-chart
chart: backstage
targetRevision: "5.8.0"
helm:
releaseName: backstage
valueFiles:
- $values/infra/values/base/backstage-values.yaml
- $values/infra/values/upc-dev/backstage-values.yaml
- repoURL: ssh://git@git.forteapps.net:2222/Forte/launchpad.git
targetRevision: HEAD
ref: values
destination:
server: https://kubernetes.default.svc
namespace: backstage
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- Validate=true
- ServerSideApply=true

View File

@@ -22,7 +22,6 @@ spec:
releaseName: gitea
valueFiles:
- $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
targetRevision: HEAD

View File

@@ -22,3 +22,4 @@ resources:
- tempo.yaml
- grafana-dashboards.yaml
- network-policies-application.yaml
- backstage.yaml

View File

@@ -22,7 +22,6 @@ spec:
releaseName: opencost
valueFiles:
- $values/infra/values/base/opencost-values.yaml
- $values/infra/values/upc-dev/opencost-values.yaml
- repoURL: git@github.com:fortedigital/sturdy-adventure.git
targetRevision: HEAD

View File

@@ -1,35 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
# Traefik: swap upc-dev → aws-dev
- target:
kind: Application
name: traefik
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/aws-dev/traefik-values.yaml
# Gitea: swap upc-dev → aws-dev
- target:
kind: Application
name: gitea
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/aws-dev/gitea-values.yaml
# OpenCost: swap upc-dev → aws-dev
- target:
kind: Application
name: opencost
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/aws-dev/opencost-values.yaml
# TODO: add patches for keycloak, grafana, secrets, enterprise-apps
# when deploying to this cluster (these are deployment-specific, not cloud-specific)

View File

@@ -1,35 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
# Traefik: swap upc-dev → aws-prod
- target:
kind: Application
name: traefik
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/aws-prod/traefik-values.yaml
# Gitea: swap upc-dev → aws-prod
- target:
kind: Application
name: gitea
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/aws-prod/gitea-values.yaml
# OpenCost: swap upc-dev → aws-prod
- target:
kind: Application
name: opencost
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/aws-prod/opencost-values.yaml
# TODO: add patches for keycloak, grafana, secrets, enterprise-apps
# when deploying to this cluster (these are deployment-specific, not cloud-specific)

View File

@@ -1,35 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
# Traefik: swap upc-dev → azure-dev
- target:
kind: Application
name: traefik
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/azure-dev/traefik-values.yaml
# Gitea: swap upc-dev → azure-dev
- target:
kind: Application
name: gitea
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/azure-dev/gitea-values.yaml
# OpenCost: swap upc-dev → azure-dev
- target:
kind: Application
name: opencost
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/azure-dev/opencost-values.yaml
# TODO: add patches for keycloak, grafana, secrets, enterprise-apps
# when deploying to this cluster (these are deployment-specific, not cloud-specific)

View File

@@ -1,35 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
# Traefik: swap upc-dev → azure-prod
- target:
kind: Application
name: traefik
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/azure-prod/traefik-values.yaml
# Gitea: swap upc-dev → azure-prod
- target:
kind: Application
name: gitea
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/azure-prod/gitea-values.yaml
# OpenCost: swap upc-dev → azure-prod
- target:
kind: Application
name: opencost
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/azure-prod/opencost-values.yaml
# TODO: add patches for keycloak, grafana, secrets, enterprise-apps
# when deploying to this cluster (these are deployment-specific, not cloud-specific)

View File

@@ -1,35 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
# Traefik: swap upc-dev → gcp-dev
- target:
kind: Application
name: traefik
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/gcp-dev/traefik-values.yaml
# Gitea: swap upc-dev → gcp-dev
- target:
kind: Application
name: gitea
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/gcp-dev/gitea-values.yaml
# OpenCost: swap upc-dev → gcp-dev
- target:
kind: Application
name: opencost
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/gcp-dev/opencost-values.yaml
# TODO: add patches for keycloak, grafana, secrets, enterprise-apps
# when deploying to this cluster (these are deployment-specific, not cloud-specific)

View File

@@ -1,35 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
# Traefik: swap upc-dev → gcp-prod
- target:
kind: Application
name: traefik
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/gcp-prod/traefik-values.yaml
# Gitea: swap upc-dev → gcp-prod
- target:
kind: Application
name: gitea
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/gcp-prod/gitea-values.yaml
# OpenCost: swap upc-dev → gcp-prod
- target:
kind: Application
name: opencost
patch: |
- op: replace
path: /spec/sources/0/helm/valueFiles/1
value: $values/infra/values/gcp-prod/opencost-values.yaml
# TODO: add patches for keycloak, grafana, secrets, enterprise-apps
# when deploying to this cluster (these are deployment-specific, not cloud-specific)

View File

@@ -48,21 +48,3 @@ patches:
- op: replace
path: /spec/source/path
value: apps/overlays/upc-prod
# 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

View File

@@ -1,7 +0,0 @@
# AWS EBS gp3 storage class (requires EBS CSI driver)
persistence:
storageClass: gp3
postgresql:
primary:
persistence:
storageClass: gp3

View File

@@ -1,13 +0,0 @@
# AWS native pricing via Cost and Usage Reports
opencost:
exporter:
customPricing:
enabled: true
provider: aws
aws:
service_key_name: "" # <- populate or use IRSA
service_key_secret: ""
spot_data_region: ""
spot_data_bucket: ""
spot_data_prefix: ""
account_id: ""

View File

@@ -1,18 +0,0 @@
# AWS EKS — NLB with Proxy Protocol v2 for real client IPs
service:
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "external"
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
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" # <- adjust to your VPC CIDR
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 @@
# AWS EBS gp3 storage class (requires EBS CSI driver)
persistence:
storageClass: gp3
postgresql:
primary:
persistence:
storageClass: gp3

View File

@@ -1,13 +0,0 @@
# AWS native pricing via Cost and Usage Reports
opencost:
exporter:
customPricing:
enabled: true
provider: aws
aws:
service_key_name: "" # <- populate or use IRSA
service_key_secret: ""
spot_data_region: ""
spot_data_bucket: ""
spot_data_prefix: ""
account_id: ""

View File

@@ -1,18 +0,0 @@
# AWS EKS — NLB with Proxy Protocol v2 for real client IPs
service:
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "external"
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
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" # <- adjust to your VPC CIDR
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 @@
# Azure Managed Disk (Premium SSD via CSI driver)
persistence:
storageClass: managed-csi-premium
postgresql:
primary:
persistence:
storageClass: managed-csi-premium

View File

@@ -1,11 +0,0 @@
# Azure native pricing via Billing API
opencost:
exporter:
customPricing:
enabled: true
provider: azure
azure:
subscriptionID: "" # <- populate
clientID: ""
clientSecret: ""
tenantID: ""

View File

@@ -1,16 +0,0 @@
# Azure AKS — Standard Load Balancer
# Note: Azure Standard LB does not support Proxy Protocol.
# Use externalTrafficPolicy: Local on the Traefik service to preserve
# client IPs, or deploy behind Azure Application Gateway.
service:
annotations:
service.beta.kubernetes.io/azure-load-balancer-health-probe-request-path: "/ping"
spec:
externalTrafficPolicy: Local
ports:
web:
forwardedHeaders:
trustedIPs: "10.0.0.0/8,168.63.129.16/32" # <- VNet CIDR + Azure health probe
websecure:
forwardedHeaders:
trustedIPs: "10.0.0.0/8,168.63.129.16/32"

View File

@@ -1,7 +0,0 @@
# Azure Managed Disk (Premium SSD via CSI driver)
persistence:
storageClass: managed-csi-premium
postgresql:
primary:
persistence:
storageClass: managed-csi-premium

View File

@@ -1,11 +0,0 @@
# Azure native pricing via Billing API
opencost:
exporter:
customPricing:
enabled: true
provider: azure
azure:
subscriptionID: "" # <- populate
clientID: ""
clientSecret: ""
tenantID: ""

View File

@@ -1,16 +0,0 @@
# Azure AKS — Standard Load Balancer
# Note: Azure Standard LB does not support Proxy Protocol.
# Use externalTrafficPolicy: Local on the Traefik service to preserve
# client IPs, or deploy behind Azure Application Gateway.
service:
annotations:
service.beta.kubernetes.io/azure-load-balancer-health-probe-request-path: "/ping"
spec:
externalTrafficPolicy: Local
ports:
web:
forwardedHeaders:
trustedIPs: "10.0.0.0/8,168.63.129.16/32" # <- VNet CIDR + Azure health probe
websecure:
forwardedHeaders:
trustedIPs: "10.0.0.0/8,168.63.129.16/32"

View File

@@ -2,12 +2,21 @@ configs:
secret:
createSecret: true
argocdServerAdminPassword: "$2b$12$Tmb1jH7ADvwWoUoNPXXsfOf6JqEluqhq8mL06a8DGT2AP1GzbNsCm"
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:
application.resourceTrackingMethod: annotation
timeout.reconciliation: 60s
admin.enabled: "true"
params:
"server.insecure": true
repoServer:
env:
# Disable git submodule checkout - submodules (e.g. shared-prompts)
# are not needed for K8s manifest generation
- name: ARGOCD_GIT_MODULES_ENABLED
value: "false"
server:
ingress:
enabled: false

View File

@@ -0,0 +1,150 @@
# Red Hat Developer Hub (RHDH) - Internal Developer Portal
# Helm chart: https://github.com/redhat-developer/rhdh-chart
# Includes 27+ plugins out of the box: ArgoCD, Kubernetes, Keycloak,
# GitHub, GitLab, Jira, SonarQube, Tekton, Jenkins, and more.
global:
auth:
backend:
enabled: true
dynamic:
includes:
- dynamic-plugins.default.yaml
plugins: []
# Disable OpenShift Route (not on OpenShift)
route:
enabled: false
upstream:
backstage:
image:
registry: quay.io
repository: rhdh-community/rhdh
tag: next
podSecurityContext:
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
extraEnvVarsSecrets:
- backstage-oidc-credentials
- backstage-session-secret
appConfig:
app:
title: "Forte Backstage"
baseUrl: http://localhost:7007
backend:
baseUrl: http://localhost:7007
# -- Keycloak OIDC authentication
signInPage: oidc
auth:
session:
secret: ${AUTH_SESSION_SECRET}
environment: production
providers:
oidc:
production:
metadataUrl: https://id.forteapps.net/realms/forte/.well-known/openid-configuration
clientId: ${AUTH_OIDC_CLIENT_ID}
clientSecret: ${AUTH_OIDC_CLIENT_SECRET}
prompt: auto
# Allow login before User entities exist in the catalog.
# Remove once org data is populated.
dangerouslyAllowSignInWithoutUserInCatalog: true
signIn:
resolvers:
- resolver: emailMatchingUserEntityProfileEmail
# -- Gitea SCM integration (for catalog URL resolution)
integrations:
gitea:
- host: git.forteapps.net
# -- Software catalog
catalog:
rules:
- allow:
- Component
- System
- API
- Resource
- Location
- Template
- Group
- User
- Domain
providers:
# Auto-import users and groups from Keycloak
keycloakOrg:
default:
baseUrl: https://id.forteapps.net
realm: forte
clientId: ${AUTH_OIDC_CLIENT_ID}
clientSecret: ${AUTH_OIDC_CLIENT_SECRET}
schedule:
frequency: { minutes: 30 }
timeout: { minutes: 3 }
initialDelay: { seconds: 15 }
# Auto-discover catalog-info.yaml from all Forte org repos
gitea:
forte:
organization: Forte
host: git.forteapps.net
catalogPath: catalog-info.yaml
schedule:
frequency: { minutes: 30 }
timeout: { minutes: 3 }
locations:
# Backstage's own org data (bootstrap teams, systems, domains)
# - type: url
# target: https://git.forteapps.net/Forte/backstage-catalog/raw/branch/main/org.yaml
# rules:
# - allow: [Group, User, System, Domain]
ingress:
enabled: true
className: traefik
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
cert-manager.io/cluster-issuer: letsencrypt-prod
tls:
enabled: true
secretName: backstage-tls
postgresql:
enabled: true
auth:
# Fixed passwords prevent Helm from regenerating the Secret on
# each sync, which would mismatch with the PVC-persisted data.
password: backstage-db-pw
postgresPassword: backstage-admin-pw
primary:
persistence:
enabled: true
size: 2Gi
podSecurityContext:
enabled: true
fsGroup: 26
runAsUser: 26
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 250m
memory: 512Mi
volumePermissions:
enabled: false

View File

@@ -130,6 +130,7 @@ persistence:
size: 10Gi
accessModes:
- ReadWriteOnce
storageClass: upcloud-block-storage-maxiops
# -- Recreate strategy to avoid Multi-Attach errors with RWO volumes
strategy:
@@ -155,6 +156,7 @@ postgresql:
persistence:
enabled: true
size: 8Gi
storageClass: upcloud-block-storage-maxiops
resources:
requests:
cpu: 100m

View File

@@ -116,12 +116,12 @@ extraDeploy:
metadata:
name: keycloak-client-registrar
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "create", "update", "patch"]
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "list"]
- apiGroups: [ "" ]
resources: [ "secrets" ]
verbs: [ "get", "list", "create", "update", "patch" ]
- apiGroups: [ "" ]
resources: [ "namespaces" ]
verbs: [ "get", "list" ]
# -- ClusterRoleBinding for the registrar ServiceAccount
- apiVersion: rbac.authorization.k8s.io/v1
@@ -158,7 +158,7 @@ extraDeploy:
containers:
- name: registrar
image: alpine:3.20
command: ["/bin/sh", "-c"]
command: [ "/bin/sh", "-c" ]
args:
- |
set -e

View File

@@ -10,8 +10,18 @@ opencost:
serviceName: prometheus-server
namespaceName: monitoring
port: 80
# Cloud-specific pricing is in per-cluster value overrides
# (e.g. infra/values/upc-dev/opencost-values.yaml)
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"
ui:
enabled: false
service:

View File

@@ -1,7 +0,0 @@
# GCP Persistent Disk (SSD via CSI driver)
persistence:
storageClass: premium-rwo
postgresql:
primary:
persistence:
storageClass: premium-rwo

View File

@@ -1,9 +0,0 @@
# GCP native pricing via Cloud Billing API
opencost:
exporter:
customPricing:
enabled: true
provider: gcp
gcp:
projectID: "" # <- populate with your GCP project ID
key: "" # <- or use Workload Identity

View File

@@ -1,15 +0,0 @@
# GCP GKE — External passthrough Network Load Balancer
service:
annotations:
cloud.google.com/l4-rbs: "enabled"
ports:
web:
proxyProtocol:
trustedIPs: "10.0.0.0/8,35.191.0.0/16,130.211.0.0/22" # <- subnet CIDR + GCP health checks
forwardedHeaders:
trustedIPs: "10.0.0.0/8,35.191.0.0/16,130.211.0.0/22"
websecure:
proxyProtocol:
trustedIPs: "10.0.0.0/8,35.191.0.0/16,130.211.0.0/22"
forwardedHeaders:
trustedIPs: "10.0.0.0/8,35.191.0.0/16,130.211.0.0/22"

View File

@@ -1,7 +0,0 @@
# GCP Persistent Disk (SSD via CSI driver)
persistence:
storageClass: premium-rwo
postgresql:
primary:
persistence:
storageClass: premium-rwo

View File

@@ -1,9 +0,0 @@
# GCP native pricing via Cloud Billing API
opencost:
exporter:
customPricing:
enabled: true
provider: gcp
gcp:
projectID: "" # <- populate with your GCP project ID
key: "" # <- or use Workload Identity

View File

@@ -1,15 +0,0 @@
# GCP GKE — External passthrough Network Load Balancer
service:
annotations:
cloud.google.com/l4-rbs: "enabled"
ports:
web:
proxyProtocol:
trustedIPs: "10.0.0.0/8,35.191.0.0/16,130.211.0.0/22" # <- subnet CIDR + GCP health checks
forwardedHeaders:
trustedIPs: "10.0.0.0/8,35.191.0.0/16,130.211.0.0/22"
websecure:
proxyProtocol:
trustedIPs: "10.0.0.0/8,35.191.0.0/16,130.211.0.0/22"
forwardedHeaders:
trustedIPs: "10.0.0.0/8,35.191.0.0/16,130.211.0.0/22"

View File

@@ -0,0 +1,12 @@
global:
host: backstage.forteapps.net
upstream:
backstage:
appConfig:
app:
baseUrl: https://backstage.forteapps.net
backend:
baseUrl: https://backstage.forteapps.net
ingress:
host: backstage.forteapps.net

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"

View File

@@ -1,94 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Gitea backup helper for AWS S3
# Uses the gitea-backup-s3 secret in the gitea namespace
# (same secret schema: S3_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_BUCKET)
#
# For AWS, S3_ENDPOINT is typically https://s3.<region>.amazonaws.com
#
# Usage:
# ./scripts/gitea-backup-aws.sh list # list all backups
# ./scripts/gitea-backup-aws.sh download <filename> # download a backup to current dir
# ./scripts/gitea-backup-aws.sh download latest # download the most recent backup
NAMESPACE="gitea"
SECRET="gitea-backup-s3"
IMAGE="minio/mc:latest"
POD_NAME="gitea-backup-helper"
ALIAS_CMD='mc alias set s3 ${S3_ENDPOINT} ${AWS_ACCESS_KEY_ID} ${AWS_SECRET_ACCESS_KEY} > /dev/null'
cleanup() {
kubectl -n "$NAMESPACE" delete pod "$POD_NAME" --ignore-not-found --grace-period=0 > /dev/null 2>&1 || true
}
mc_run() {
cleanup
kubectl -n "$NAMESPACE" run "$POD_NAME" --restart=Never \
--image="$IMAGE" \
--overrides="{
\"spec\":{\"containers\":[{
\"name\":\"$POD_NAME\",
\"image\":\"$IMAGE\",
\"env\":[{\"name\":\"HOME\",\"value\":\"/tmp\"}],
\"command\":[\"sh\",\"-c\",\"${ALIAS_CMD}; $1\"],
\"envFrom\":[{\"secretRef\":{\"name\":\"$SECRET\"}}]
}]}
}" > /dev/null 2>&1
kubectl -n "$NAMESPACE" wait --for=jsonpath='{.status.phase}'=Succeeded "pod/$POD_NAME" --timeout=120s > /dev/null 2>&1
kubectl -n "$NAMESPACE" logs "$POD_NAME"
cleanup
}
case "${1:-help}" in
list)
echo "Listing backups..."
mc_run 'mc ls s3/${S3_BUCKET}/'
;;
download)
FILE="${2:?Usage: $0 download <filename|latest>}"
if [ "$FILE" = "latest" ]; then
echo "Finding latest backup..."
FILE=$(mc_run 'mc ls s3/${S3_BUCKET}/' | sort | tail -1 | awk '{print $NF}' | tr -d '[:space:]')
if [ -z "$FILE" ]; then
echo "No backups found."
exit 1
fi
echo "Latest: $FILE"
fi
echo "Downloading $FILE..."
cleanup
kubectl -n "$NAMESPACE" run "$POD_NAME" --restart=Never \
--image="$IMAGE" \
--overrides="{
\"spec\":{\"containers\":[{
\"name\":\"$POD_NAME\",
\"image\":\"$IMAGE\",
\"env\":[{\"name\":\"HOME\",\"value\":\"/tmp\"}],
\"command\":[\"sh\",\"-c\",\"sleep 300\"],
\"envFrom\":[{\"secretRef\":{\"name\":\"$SECRET\"}}]
}]}
}" > /dev/null 2>&1
kubectl -n "$NAMESPACE" wait --for=condition=Ready "pod/$POD_NAME" --timeout=60s > /dev/null 2>&1
echo "Saving to ./$FILE ..."
kubectl -n "$NAMESPACE" exec "$POD_NAME" -- sh -c "${ALIAS_CMD} && mc cat s3/\${S3_BUCKET}/$FILE" > "./$FILE"
cleanup
echo "Downloaded: ./$FILE"
;;
*)
echo "Gitea backup helper (AWS S3)"
echo ""
echo "Usage:"
echo " $0 list List all backups in S3"
echo " $0 download <filename> Download a specific backup"
echo " $0 download latest Download the most recent backup"
;;
esac

View File

@@ -1,100 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Gitea backup helper for Azure Blob Storage
# Uses the gitea-backup-azure secret in the gitea namespace
# Required secret keys:
# AZURE_STORAGE_ACCOUNT — storage account name
# AZURE_STORAGE_KEY — storage account key
# AZURE_CONTAINER — blob container name
#
# Usage:
# ./scripts/gitea-backup-azure.sh list # list all backups
# ./scripts/gitea-backup-azure.sh download <filename> # download a backup
# ./scripts/gitea-backup-azure.sh download latest # download the most recent backup
NAMESPACE="gitea"
SECRET="gitea-backup-azure"
IMAGE="mcr.microsoft.com/azure-cli:latest"
POD_NAME="gitea-backup-helper"
cleanup() {
kubectl -n "$NAMESPACE" delete pod "$POD_NAME" --ignore-not-found --grace-period=0 > /dev/null 2>&1 || true
}
az_run() {
cleanup
kubectl -n "$NAMESPACE" run "$POD_NAME" --restart=Never \
--image="$IMAGE" \
--overrides="{
\"spec\":{\"containers\":[{
\"name\":\"$POD_NAME\",
\"image\":\"$IMAGE\",
\"env\":[{\"name\":\"HOME\",\"value\":\"/tmp\"}],
\"command\":[\"sh\",\"-c\",\"$1\"],
\"envFrom\":[{\"secretRef\":{\"name\":\"$SECRET\"}}]
}]}
}" > /dev/null 2>&1
kubectl -n "$NAMESPACE" wait --for=jsonpath='{.status.phase}'=Succeeded "pod/$POD_NAME" --timeout=120s > /dev/null 2>&1
kubectl -n "$NAMESPACE" logs "$POD_NAME"
cleanup
}
case "${1:-help}" in
list)
echo "Listing backups..."
az_run 'az storage blob list --account-name ${AZURE_STORAGE_ACCOUNT} --account-key ${AZURE_STORAGE_KEY} --container-name ${AZURE_CONTAINER} --output table --query "[].{Name:name, Size:properties.contentLength, Modified:properties.lastModified}"'
;;
download)
FILE="${2:?Usage: $0 download <filename|latest>}"
if [ "$FILE" = "latest" ]; then
echo "Finding latest backup..."
FILE=$(az_run 'az storage blob list --account-name ${AZURE_STORAGE_ACCOUNT} --account-key ${AZURE_STORAGE_KEY} --container-name ${AZURE_CONTAINER} --query "sort_by([], &properties.lastModified)[-1].name" -o tsv' | tr -d '[:space:]')
if [ -z "$FILE" ]; then
echo "No backups found."
exit 1
fi
echo "Latest: $FILE"
fi
echo "Downloading $FILE..."
cleanup
kubectl -n "$NAMESPACE" run "$POD_NAME" --restart=Never \
--image="$IMAGE" \
--overrides="{
\"spec\":{\"containers\":[{
\"name\":\"$POD_NAME\",
\"image\":\"$IMAGE\",
\"env\":[{\"name\":\"HOME\",\"value\":\"/tmp\"}],
\"command\":[\"sh\",\"-c\",\"sleep 300\"],
\"envFrom\":[{\"secretRef\":{\"name\":\"$SECRET\"}}]
}]}
}" > /dev/null 2>&1
kubectl -n "$NAMESPACE" wait --for=condition=Ready "pod/$POD_NAME" --timeout=60s > /dev/null 2>&1
echo "Saving to ./$FILE ..."
kubectl -n "$NAMESPACE" exec "$POD_NAME" -- \
az storage blob download \
--account-name "\${AZURE_STORAGE_ACCOUNT}" \
--account-key "\${AZURE_STORAGE_KEY}" \
--container-name "\${AZURE_CONTAINER}" \
--name "$FILE" \
--file /dev/stdout 2>/dev/null > "./$FILE"
cleanup
echo "Downloaded: ./$FILE"
;;
*)
echo "Gitea backup helper (Azure Blob Storage)"
echo ""
echo "Usage:"
echo " $0 list List all backups in Azure Blob"
echo " $0 download <filename> Download a specific backup"
echo " $0 download latest Download the most recent backup"
;;
esac

View File

@@ -1,95 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Gitea backup helper for Google Cloud Storage
# Uses the gitea-backup-gcs secret in the gitea namespace
# Required secret keys:
# GCS_BUCKET — bucket name (without gs:// prefix)
# GOOGLE_APPLICATION_CREDENTIALS_JSON — service account key JSON
# (alternatively, use Workload Identity and omit the key)
#
# Usage:
# ./scripts/gitea-backup-gcp.sh list # list all backups
# ./scripts/gitea-backup-gcp.sh download <filename> # download a backup
# ./scripts/gitea-backup-gcp.sh download latest # download the most recent backup
NAMESPACE="gitea"
SECRET="gitea-backup-gcs"
IMAGE="gcr.io/google.com/cloudsdktool/google-cloud-cli:slim"
POD_NAME="gitea-backup-helper"
AUTH_CMD='if [ -n "${GOOGLE_APPLICATION_CREDENTIALS_JSON:-}" ]; then echo "${GOOGLE_APPLICATION_CREDENTIALS_JSON}" > /tmp/gcs-key.json && gcloud auth activate-service-account --key-file=/tmp/gcs-key.json > /dev/null 2>&1; fi'
cleanup() {
kubectl -n "$NAMESPACE" delete pod "$POD_NAME" --ignore-not-found --grace-period=0 > /dev/null 2>&1 || true
}
gcs_run() {
cleanup
kubectl -n "$NAMESPACE" run "$POD_NAME" --restart=Never \
--image="$IMAGE" \
--overrides="{
\"spec\":{\"containers\":[{
\"name\":\"$POD_NAME\",
\"image\":\"$IMAGE\",
\"env\":[{\"name\":\"HOME\",\"value\":\"/tmp\"}],
\"command\":[\"sh\",\"-c\",\"${AUTH_CMD}; $1\"],
\"envFrom\":[{\"secretRef\":{\"name\":\"$SECRET\"}}]
}]}
}" > /dev/null 2>&1
kubectl -n "$NAMESPACE" wait --for=jsonpath='{.status.phase}'=Succeeded "pod/$POD_NAME" --timeout=120s > /dev/null 2>&1
kubectl -n "$NAMESPACE" logs "$POD_NAME"
cleanup
}
case "${1:-help}" in
list)
echo "Listing backups..."
gcs_run 'gsutil ls -l gs://${GCS_BUCKET}/'
;;
download)
FILE="${2:?Usage: $0 download <filename|latest>}"
if [ "$FILE" = "latest" ]; then
echo "Finding latest backup..."
FILE=$(gcs_run 'gsutil ls gs://${GCS_BUCKET}/' | grep -v '^$' | grep -v 'TOTAL' | sort | tail -1 | xargs -I{} basename {} | tr -d '[:space:]')
if [ -z "$FILE" ]; then
echo "No backups found."
exit 1
fi
echo "Latest: $FILE"
fi
echo "Downloading $FILE..."
cleanup
kubectl -n "$NAMESPACE" run "$POD_NAME" --restart=Never \
--image="$IMAGE" \
--overrides="{
\"spec\":{\"containers\":[{
\"name\":\"$POD_NAME\",
\"image\":\"$IMAGE\",
\"env\":[{\"name\":\"HOME\",\"value\":\"/tmp\"}],
\"command\":[\"sh\",\"-c\",\"sleep 300\"],
\"envFrom\":[{\"secretRef\":{\"name\":\"$SECRET\"}}]
}]}
}" > /dev/null 2>&1
kubectl -n "$NAMESPACE" wait --for=condition=Ready "pod/$POD_NAME" --timeout=60s > /dev/null 2>&1
echo "Saving to ./$FILE ..."
kubectl -n "$NAMESPACE" exec "$POD_NAME" -- sh -c "${AUTH_CMD} && gsutil cat gs://\${GCS_BUCKET}/$FILE" > "./$FILE"
cleanup
echo "Downloaded: ./$FILE"
;;
*)
echo "Gitea backup helper (Google Cloud Storage)"
echo ""
echo "Usage:"
echo " $0 list List all backups in GCS"
echo " $0 download <filename> Download a specific backup"
echo " $0 download latest Download the most recent backup"
;;
esac