feat: initial scaffold from helm-template
Validate login GitOps repo / validate (push) Successful in 9s
Validate login GitOps repo / validate (push) Successful in 9s
- Keycloak Operator v26 deployed to keycloak-system namespace - Keycloak HA instance (2 replicas, jdbc-ping cluster discovery) - Dedicated CNPG cluster (local-postgres storage, 2 instances) - KeycloakRealmImport: home-lab realm with groups and traefik-oidc client - Traefik IngressRoute: login.olb42.com (CF Access bypass) - Admin credentials placeholder (seal before first deploy) - ArgoCD ApplicationSets: login-operator + login
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
name: Propose dormant overlay change
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- dormant/**
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
propose-dormant-pr:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Run dormant PR automation
|
||||||
|
run: scripts/automation/propose-dormant-pr.sh
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: Validate login GitOps repo
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y bash curl ca-certificates git python3 python3-yaml
|
||||||
|
curl -sL https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz \
|
||||||
|
| tar xz -C /usr/local/bin
|
||||||
|
curl -fsSL "https://dl.k8s.io/release/$(curl -fsSL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
|
||||||
|
-o /usr/local/bin/kubectl
|
||||||
|
chmod +x /usr/local/bin/kubectl
|
||||||
|
|
||||||
|
- name: Validate repo
|
||||||
|
run: scripts/ci/validate
|
||||||
|
|
||||||
|
- name: Apply bootstrap ApplicationSets
|
||||||
|
env:
|
||||||
|
KUBECONFIG_B64: ${{ secrets.KUBECONFIG_B64 }}
|
||||||
|
run: scripts/ci/apply-bootstrap
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
# Local secret files — never commit plain secrets
|
||||||
|
*.secret.plain.yaml
|
||||||
|
*.plain.yaml
|
||||||
|
|
||||||
|
# CI tooling caches
|
||||||
|
.ci-schemas/
|
||||||
|
.ci-rendered/
|
||||||
|
.ci-chart-cache/
|
||||||
|
.helm-cache/
|
||||||
|
.helm-config/
|
||||||
|
.helm-data/
|
||||||
|
.kube/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# login — Keycloak Operator-managed instance
|
||||||
|
|
||||||
|
Keycloak HA deployment for `login.olb42.com`, managed by the official
|
||||||
|
Keycloak Operator. Backs all OIDC authentication across the home-lab.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- **Namespace**: `login`
|
||||||
|
- **Operator namespace**: `keycloak-system`
|
||||||
|
- **Database**: CNPG cluster `login-postgres` (2 instances, `local-postgres` storage)
|
||||||
|
- **Replicas**: 2 Keycloak pods with Infinispan jdbc-ping cluster discovery
|
||||||
|
- **Ingress**: `login.olb42.com` via Traefik IngressRoute (no CF Access — bypass)
|
||||||
|
|
||||||
|
## Bootstrap order
|
||||||
|
|
||||||
|
1. ArgoCD applies `login-operator` ApplicationSet → Keycloak Operator deployed
|
||||||
|
2. ArgoCD applies `login` ApplicationSet → CRDs reconciled:
|
||||||
|
- CNPG Cluster `login-postgres` created
|
||||||
|
- `Keycloak` CR reconciled → Operator deploys Keycloak pods
|
||||||
|
- `KeycloakRealmImport` applied → `home-lab` realm created
|
||||||
|
- IngressRoute activates `login.olb42.com`
|
||||||
|
|
||||||
|
## Sealing the admin secret
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Edit admin.secret.plain.yaml with your chosen password, then:
|
||||||
|
kubeseal --context homelab-argocd \
|
||||||
|
--secret-file manifest/overlays/production/admin.secret.plain.yaml \
|
||||||
|
--sealed-secret-file manifest/overlays/production/admin.sealed.secret.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding a per-app Keycloak client
|
||||||
|
|
||||||
|
Create a `KeycloakRealmImport` in the app's own repo:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: k8s.keycloak.org/v2alpha1
|
||||||
|
kind: KeycloakRealmImport
|
||||||
|
metadata:
|
||||||
|
name: my-app-client
|
||||||
|
namespace: login # must match the Keycloak CR namespace
|
||||||
|
spec:
|
||||||
|
keycloakCRName: keycloak # must match the Keycloak CR name
|
||||||
|
realm:
|
||||||
|
realm: home-lab
|
||||||
|
clients:
|
||||||
|
- clientId: traefik-oidc-my-app
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
ArgoCD syncs the `KeycloakRealmImport` → Operator registers the client in
|
||||||
|
Keycloak → Operator writes `traefik-oidc-my-app` secret → traefikoidc
|
||||||
|
Middleware references it.
|
||||||
|
|
||||||
|
## Upgrading Keycloak
|
||||||
|
|
||||||
|
1. Update `image: quay.io/keycloak/keycloak:<new-version>` in `keycloak-instance.yaml`
|
||||||
|
2. Update the operator remote URL version in `manifest/operator/kustomization.yaml`
|
||||||
|
3. Commit and push — ArgoCD self-heals both ApplicationSets in order
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# login
|
||||||
|
|
||||||
|
Operator-managed Keycloak HA deployment. Identity and access management for all home-lab services.
|
||||||
|
|
||||||
|
- **URL**: https://login.olb42.com
|
||||||
|
- **Realm**: `home-lab`
|
||||||
|
- **Operator**: [keycloak-operator](https://github.com/keycloak/keycloak-operator) v26
|
||||||
|
- **Database**: CNPG PostgreSQL (`local-postgres` storage class)
|
||||||
|
|
||||||
|
See [OPERATIONS.md](./OPERATIONS.md) for bootstrap, secret sealing, and client management procedures.
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# ── ApplicationSet 1: Keycloak Operator (CRDs + controller) ──────────────────
|
||||||
|
# Deploys the Keycloak Operator into its own namespace so it can manage
|
||||||
|
# Keycloak instances cluster-wide. The operator install includes cluster-scoped
|
||||||
|
# resources (CRDs, ClusterRoles) which must be applied with ServerSideApply.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: ApplicationSet
|
||||||
|
metadata:
|
||||||
|
name: login-operator
|
||||||
|
namespace: argocd
|
||||||
|
spec:
|
||||||
|
ignoreApplicationDifferences:
|
||||||
|
- jsonPointers:
|
||||||
|
- /spec/syncPolicy
|
||||||
|
goTemplate: true
|
||||||
|
goTemplateOptions: ["missingkey=error"]
|
||||||
|
generators:
|
||||||
|
- list:
|
||||||
|
elements:
|
||||||
|
- environment: production
|
||||||
|
namespace: keycloak-system
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
name: 'login-operator-{{ .environment }}'
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/managed-by: argocd
|
||||||
|
app.kubernetes.io/name: login-operator
|
||||||
|
spec:
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
repoURL: http://gitea-ha-http.apps:3000/olb42/login.git
|
||||||
|
targetRevision: main
|
||||||
|
path: manifest/operator
|
||||||
|
kustomize:
|
||||||
|
commonAnnotationsEnvsubst: true
|
||||||
|
commonAnnotations:
|
||||||
|
app-source: ${ARGOCD_APP_SOURCE_REPO_URL}
|
||||||
|
app-revision: ${ARGOCD_APP_SOURCE_TARGET_REVISION}
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: '{{ .namespace }}'
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: false
|
||||||
|
selfHeal: true
|
||||||
|
enabled: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
- ApplyOutOfSyncOnly=true
|
||||||
|
- ServerSideApply=true
|
||||||
|
- Replace=false
|
||||||
|
---
|
||||||
|
# ── ApplicationSet 2: login Keycloak instance ────────────────────────────────
|
||||||
|
# Deploys the Keycloak CR, CNPG cluster, IngressRoute, and realm config.
|
||||||
|
# Depends on login-operator being synced first (CRDs must exist).
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: ApplicationSet
|
||||||
|
metadata:
|
||||||
|
name: login
|
||||||
|
namespace: argocd
|
||||||
|
spec:
|
||||||
|
ignoreApplicationDifferences:
|
||||||
|
- jsonPointers:
|
||||||
|
- /spec/syncPolicy
|
||||||
|
goTemplate: true
|
||||||
|
goTemplateOptions: ["missingkey=error"]
|
||||||
|
generators:
|
||||||
|
- list:
|
||||||
|
elements:
|
||||||
|
- environment: production
|
||||||
|
namespace: login
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
name: 'login-{{ .environment }}'
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/managed-by: argocd
|
||||||
|
app.kubernetes.io/name: login
|
||||||
|
spec:
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
repoURL: http://gitea-ha-http.apps:3000/olb42/login.git
|
||||||
|
targetRevision: main
|
||||||
|
path: 'manifest/overlays/{{ .environment }}'
|
||||||
|
kustomize:
|
||||||
|
commonAnnotationsEnvsubst: true
|
||||||
|
commonAnnotations:
|
||||||
|
app-source: ${ARGOCD_APP_SOURCE_REPO_URL}
|
||||||
|
app-revision: ${ARGOCD_APP_SOURCE_TARGET_REVISION}
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: '{{ .namespace }}'
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
enabled: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
- ApplyOutOfSyncOnly=true
|
||||||
|
- ServerSideApply=true
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
enabled: true
|
||||||
|
target_namespace: argocd
|
||||||
|
apply_from_branch: main
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- runtime
|
||||||
|
- state
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
# Runtime objects are managed by the Keycloak Operator via the Keycloak CR.
|
||||||
|
# Nothing to define here — the operator creates Deployments, Services, and
|
||||||
|
# ConfigMaps from the spec in manifest/overlays/production/keycloak-instance.yaml.
|
||||||
|
resources: []
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
# Persistent state is managed by the CNPG Cluster CR defined in the production
|
||||||
|
# overlay. CNPG handles PVC lifecycle; nothing additional is needed here.
|
||||||
|
resources: []
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
# Keycloak Operator v26 — official Kubernetes manifests.
|
||||||
|
# Pin to a specific release tag; bump here when upgrading Keycloak.
|
||||||
|
# Full release list: https://github.com/keycloak/keycloak-k8s-resources/releases
|
||||||
|
#
|
||||||
|
# These resources include:
|
||||||
|
# - keycloaks.k8s.keycloak.org CRD
|
||||||
|
# - keycloakrealmimports.k8s.keycloak.org CRD
|
||||||
|
# - keycloak-operator Deployment + RBAC (deployed to keycloak-system namespace
|
||||||
|
# as defined in the upstream manifest; operator watches all namespaces)
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.2.5/kubernetes/keycloaks.k8s.keycloak.org-v1.yml
|
||||||
|
- https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.2.5/kubernetes/keycloakrealmimports.k8s.keycloak.org-v1.yml
|
||||||
|
- https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.2.5/kubernetes/kubernetes.yml
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
namespace: login
|
||||||
|
|
||||||
|
# Dormant mode: CNPG cluster stays alive to preserve data.
|
||||||
|
# Keycloak instance is scaled to zero by patching the Keycloak CR.
|
||||||
|
resources:
|
||||||
|
- ../../base/state
|
||||||
|
|
||||||
|
patches:
|
||||||
|
- patch: |
|
||||||
|
- op: replace
|
||||||
|
path: /spec/instances
|
||||||
|
value: 0
|
||||||
|
target:
|
||||||
|
kind: Keycloak
|
||||||
|
name: keycloak
|
||||||
|
|
||||||
|
commonLabels:
|
||||||
|
app.kubernetes.io/part-of: login
|
||||||
|
app.kubernetes.io/environment: dormant
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
apiVersion: bitnami.com/v1alpha1
|
||||||
|
kind: SealedSecret
|
||||||
|
metadata:
|
||||||
|
name: login-admin
|
||||||
|
namespace: login
|
||||||
|
annotations:
|
||||||
|
sealedsecrets.bitnami.com/managed: "true"
|
||||||
|
spec:
|
||||||
|
encryptedData:
|
||||||
|
# Seal from admin.secret.plain.yaml:
|
||||||
|
# kubeseal --context homelab-argocd \
|
||||||
|
# --secret-file manifest/overlays/production/admin.secret.plain.yaml \
|
||||||
|
# --sealed-secret-file manifest/overlays/production/admin.sealed.secret.yaml
|
||||||
|
username: REPLACE_WITH_KUBESEAL_OUTPUT
|
||||||
|
password: REPLACE_WITH_KUBESEAL_OUTPUT
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
name: login-admin
|
||||||
|
namespace: login
|
||||||
|
annotations:
|
||||||
|
sealedsecrets.bitnami.com/managed: "true"
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/part-of: login
|
||||||
|
app.kubernetes.io/environment: production
|
||||||
|
type: Opaque
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
apiVersion: postgresql.cnpg.io/v1
|
||||||
|
kind: Cluster
|
||||||
|
metadata:
|
||||||
|
name: login-postgres
|
||||||
|
namespace: login
|
||||||
|
spec:
|
||||||
|
# ── HA: 1 primary + 1 standby replica ────────────────────────────────────
|
||||||
|
instances: 2
|
||||||
|
|
||||||
|
# ── Storage: local-postgres class (fast local NVMe, no Longhorn overhead) ─
|
||||||
|
storage:
|
||||||
|
storageClass: local-postgres
|
||||||
|
size: 8Gi
|
||||||
|
|
||||||
|
# ── Spread replicas across nodes ──────────────────────────────────────────
|
||||||
|
affinity:
|
||||||
|
enablePodAntiAffinity: true
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
|
||||||
|
# ── Bootstrap: create the keycloak database and owner role ───────────────
|
||||||
|
bootstrap:
|
||||||
|
initdb:
|
||||||
|
database: keycloak
|
||||||
|
owner: keycloak
|
||||||
|
|
||||||
|
# ── WAL archival via barman-cloud → MinIO ─────────────────────────────────
|
||||||
|
plugins:
|
||||||
|
- name: barman-cloud.cloudnative-pg.io
|
||||||
|
isWALArchiver: true
|
||||||
|
parameters:
|
||||||
|
barmanObjectName: minio-store
|
||||||
|
|
||||||
|
# ── PostgreSQL tuning for Keycloak workload ───────────────────────────────
|
||||||
|
postgresql:
|
||||||
|
parameters:
|
||||||
|
max_connections: "200"
|
||||||
|
shared_buffers: "256MB"
|
||||||
|
work_mem: "8MB"
|
||||||
|
maintenance_work_mem: "64MB"
|
||||||
|
|
||||||
|
# ── Resources ─────────────────────────────────────────────────────────────
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 256Mi
|
||||||
|
cpu: 100m
|
||||||
|
limits:
|
||||||
|
memory: 512Mi
|
||||||
|
cpu: 500m
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: login
|
||||||
|
namespace: login
|
||||||
|
annotations:
|
||||||
|
# Cloudflare tunnel: public access, bypass CF Access
|
||||||
|
# Keycloak IS the auth provider — it must be reachable before any OIDC
|
||||||
|
# flow can begin, so CF Access must not gate it.
|
||||||
|
dns.public: "true"
|
||||||
|
dns.public.access.policy: bypass
|
||||||
|
dns.public.hostname: login.olb42.com
|
||||||
|
|
||||||
|
# Homepage service discovery
|
||||||
|
gethomepage.dev/enabled: "true"
|
||||||
|
gethomepage.dev/name: "Keycloak"
|
||||||
|
gethomepage.dev/group: "Security"
|
||||||
|
gethomepage.dev/description: "Identity & Access Management (operator-managed)"
|
||||||
|
gethomepage.dev/href: "https://login.olb42.com"
|
||||||
|
gethomepage.dev/icon: "keycloak"
|
||||||
|
gethomepage.dev/app: "keycloak"
|
||||||
|
gethomepage.dev/namespace: "login"
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`login.olb42.com`)
|
||||||
|
kind: Rule
|
||||||
|
# No OIDC middleware: Keycloak is the identity provider itself.
|
||||||
|
# Traefik passes traffic straight through to the Keycloak service.
|
||||||
|
services:
|
||||||
|
- name: keycloak-service
|
||||||
|
namespace: login
|
||||||
|
port: 8080
|
||||||
|
tls:
|
||||||
|
secretName: olb42-wildcard-tls
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
apiVersion: k8s.keycloak.org/v2alpha1
|
||||||
|
kind: Keycloak
|
||||||
|
metadata:
|
||||||
|
name: keycloak
|
||||||
|
namespace: login
|
||||||
|
spec:
|
||||||
|
# ── HA: 2 replicas with Infinispan jdbc-ping cluster discovery ────────────
|
||||||
|
instances: 2
|
||||||
|
|
||||||
|
# ── Image: pin to the same major as the operator release ─────────────────
|
||||||
|
image: quay.io/keycloak/keycloak:26.2.5
|
||||||
|
startOptimized: false
|
||||||
|
|
||||||
|
# ── Database: CNPG cluster (login-postgres) ───────────────────────────────
|
||||||
|
# CNPG auto-creates the secret <cluster-name>-app with username/password.
|
||||||
|
db:
|
||||||
|
vendor: postgres
|
||||||
|
host: login-postgres-rw
|
||||||
|
port: 5432
|
||||||
|
database: keycloak
|
||||||
|
usernameSecret:
|
||||||
|
name: login-postgres-app
|
||||||
|
key: username
|
||||||
|
passwordSecret:
|
||||||
|
name: login-postgres-app
|
||||||
|
key: password
|
||||||
|
|
||||||
|
# ── HTTP: plain HTTP backend; TLS terminated at Traefik ───────────────────
|
||||||
|
http:
|
||||||
|
httpEnabled: true
|
||||||
|
httpPort: 8080
|
||||||
|
httpsPort: 8443
|
||||||
|
# No TLS secret — Traefik handles TLS via wildcard cert
|
||||||
|
|
||||||
|
# ── Hostname ──────────────────────────────────────────────────────────────
|
||||||
|
hostname:
|
||||||
|
hostname: login.olb42.com
|
||||||
|
admin: login.olb42.com
|
||||||
|
# strict=false: allow X-Forwarded-Host from Traefik
|
||||||
|
strict: false
|
||||||
|
strictBackchannel: false
|
||||||
|
|
||||||
|
# ── Proxy: trust forwarded headers from Traefik ──────────────────────────
|
||||||
|
proxy:
|
||||||
|
headers: forwarded
|
||||||
|
|
||||||
|
# ── Bootstrap admin credentials (sealed secret — see admin.sealed.secret.yaml)
|
||||||
|
bootstrapAdminSpec:
|
||||||
|
secret:
|
||||||
|
name: login-admin
|
||||||
|
|
||||||
|
# ── Additional Keycloak options ───────────────────────────────────────────
|
||||||
|
additionalOptions:
|
||||||
|
# jdbc-ping: DB-backed cluster discovery — no Kubernetes API/RBAC needed
|
||||||
|
- name: cache-stack
|
||||||
|
value: jdbc-ping
|
||||||
|
- name: cache
|
||||||
|
value: ispn
|
||||||
|
- name: log-level
|
||||||
|
value: INFO
|
||||||
|
# Forward client IP through Traefik X-Forwarded-For chain
|
||||||
|
- name: proxy-trusted-addresses
|
||||||
|
value: "0.0.0.0/0"
|
||||||
|
|
||||||
|
# ── Resources ─────────────────────────────────────────────────────────────
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 512Mi
|
||||||
|
cpu: 250m
|
||||||
|
limits:
|
||||||
|
memory: 1500Mi
|
||||||
|
cpu: "2"
|
||||||
|
|
||||||
|
# ── Ingress disabled: Traefik IngressRoute manages external access ─────────
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
apiVersion: k8s.keycloak.org/v2alpha1
|
||||||
|
kind: KeycloakRealmImport
|
||||||
|
metadata:
|
||||||
|
name: home-lab
|
||||||
|
namespace: login
|
||||||
|
spec:
|
||||||
|
keycloakCRName: keycloak
|
||||||
|
|
||||||
|
# ── Realm definition ──────────────────────────────────────────────────────
|
||||||
|
# This is a Keycloak realm export JSON embedded as a structured spec.
|
||||||
|
# The operator applies this on startup and reconciles on changes.
|
||||||
|
# Add clients, groups, and roles here; per-app clients can be managed in
|
||||||
|
# their own app repos by adding additional KeycloakRealmImport resources
|
||||||
|
# targeting this same keycloakCRName.
|
||||||
|
realm:
|
||||||
|
realm: home-lab
|
||||||
|
displayName: "Home Lab"
|
||||||
|
enabled: true
|
||||||
|
loginWithEmailAllowed: true
|
||||||
|
duplicateEmailsAllowed: false
|
||||||
|
resetPasswordAllowed: true
|
||||||
|
editUsernameAllowed: false
|
||||||
|
bruteForceProtected: true
|
||||||
|
permanentLockout: false
|
||||||
|
maxFailureWaitSeconds: 900
|
||||||
|
minimumQuickLoginWaitSeconds: 60
|
||||||
|
waitIncrementSeconds: 60
|
||||||
|
quickLoginCheckMilliSeconds: 1000
|
||||||
|
maxDeltaTimeSeconds: 43200
|
||||||
|
failureFactor: 10
|
||||||
|
|
||||||
|
# ── Session settings ──────────────────────────────────────────────────
|
||||||
|
ssoSessionIdleTimeout: 1800
|
||||||
|
ssoSessionMaxLifespan: 36000
|
||||||
|
accessTokenLifespan: 300
|
||||||
|
accessTokenLifespanForImplicitFlow: 900
|
||||||
|
offlineSessionIdleTimeout: 2592000
|
||||||
|
offlineSessionMaxLifespanEnabled: false
|
||||||
|
|
||||||
|
# ── Password policy ───────────────────────────────────────────────────
|
||||||
|
passwordPolicy: "length(12) and notUsername(undefined)"
|
||||||
|
|
||||||
|
# ── Groups ────────────────────────────────────────────────────────────
|
||||||
|
groups:
|
||||||
|
- name: homelab-admin
|
||||||
|
path: /homelab-admin
|
||||||
|
- name: homelab-user
|
||||||
|
path: /homelab-user
|
||||||
|
- name: transmission-users
|
||||||
|
path: /transmission-users
|
||||||
|
- name: media-users
|
||||||
|
path: /media-users
|
||||||
|
- name: monitoring-users
|
||||||
|
path: /monitoring-users
|
||||||
|
|
||||||
|
# ── Default client scopes ─────────────────────────────────────────────
|
||||||
|
defaultDefaultClientScopes:
|
||||||
|
- web-origins
|
||||||
|
- acr
|
||||||
|
- roles
|
||||||
|
- profile
|
||||||
|
- basic
|
||||||
|
- email
|
||||||
|
|
||||||
|
# ── Clients ───────────────────────────────────────────────────────────
|
||||||
|
# traefik-oidc: shared client for traefikoidc middleware (SSO cookie domain)
|
||||||
|
# Per-app clients with dedicated redirect URIs and claim mappers should be
|
||||||
|
# added as additional KeycloakRealmImport resources in each app's repo.
|
||||||
|
clients:
|
||||||
|
- clientId: traefik-oidc
|
||||||
|
name: "Traefik OIDC"
|
||||||
|
description: "Shared OIDC client for Traefik traefikoidc middleware"
|
||||||
|
enabled: true
|
||||||
|
publicClient: false
|
||||||
|
standardFlowEnabled: true
|
||||||
|
implicitFlowEnabled: false
|
||||||
|
directAccessGrantsEnabled: false
|
||||||
|
serviceAccountsEnabled: false
|
||||||
|
authorizationServicesEnabled: false
|
||||||
|
redirectUris:
|
||||||
|
- "https://transmission-ng.olb42.com/oauth2/callback"
|
||||||
|
- "https://home.olb42.com/oauth2/callback"
|
||||||
|
- "https://dozzle.olb42.com/oauth2/callback"
|
||||||
|
- "https://pgadmin4.olb42.com/oauth2/callback"
|
||||||
|
webOrigins:
|
||||||
|
- "+"
|
||||||
|
attributes:
|
||||||
|
pkce.code.challenge.method: "S256"
|
||||||
|
post.logout.redirect.uris: "+"
|
||||||
|
protocolMappers:
|
||||||
|
- name: groups
|
||||||
|
protocol: openid-connect
|
||||||
|
protocolMapper: oidc-group-membership-mapper
|
||||||
|
consentRequired: false
|
||||||
|
config:
|
||||||
|
full.path: "false"
|
||||||
|
id.token.claim: "true"
|
||||||
|
access.token.claim: "true"
|
||||||
|
claim.name: "groups"
|
||||||
|
userinfo.token.claim: "true"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
namespace: login
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- ../../base/state
|
||||||
|
- cnpg-cluster.yaml
|
||||||
|
- keycloak-instance.yaml
|
||||||
|
- keycloak-realm-home-lab.yaml
|
||||||
|
- ingressroute.yaml
|
||||||
|
- admin.sealed.secret.yaml
|
||||||
|
|
||||||
|
commonLabels:
|
||||||
|
app.kubernetes.io/part-of: login
|
||||||
|
app.kubernetes.io/environment: production
|
||||||
Executable
+35
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Copied from helm-template; update GITEA_API_URL and repo details as needed.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GITEA_API_URL="${GITEA_API_URL:-https://git.olb42.com/api/v1}"
|
||||||
|
REPO_OWNER="${REPO_OWNER:-olb42}"
|
||||||
|
REPO_NAME="${REPO_NAME:-login}"
|
||||||
|
PR_BRANCH="dormant/$(date +%Y%m%d-%H%M%S)"
|
||||||
|
BASE_BRANCH="main"
|
||||||
|
|
||||||
|
if [[ -z "${GITEA_TOKEN:-}" ]]; then
|
||||||
|
echo "GITEA_TOKEN is required"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git config user.email "[email protected]"
|
||||||
|
git config user.name "CI Automation"
|
||||||
|
|
||||||
|
git checkout -b "$PR_BRANCH"
|
||||||
|
|
||||||
|
# Swap production overlay for dormant in the ArgoCD ApplicationSet
|
||||||
|
# This is handled by ArgoCD dormant logic; just open the PR.
|
||||||
|
git push origin "$PR_BRANCH"
|
||||||
|
|
||||||
|
curl -s -X POST "$GITEA_API_URL/repos/$REPO_OWNER/$REPO_NAME/pulls" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"title\": \"Propose dormant mode for login\",
|
||||||
|
\"body\": \"Automated PR to switch login Keycloak to dormant overlay.\",
|
||||||
|
\"head\": \"$PR_BRANCH\",
|
||||||
|
\"base\": \"$BASE_BRANCH\"
|
||||||
|
}"
|
||||||
|
|
||||||
|
echo "Dormant PR created"
|
||||||
Executable
+35
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
event_name="${GITHUB_EVENT_NAME:-}"
|
||||||
|
git_ref="${GITHUB_REF:-}"
|
||||||
|
|
||||||
|
if [[ "$event_name" != "push" || "$git_ref" != "refs/heads/main" ]]; then
|
||||||
|
echo "Skipping bootstrap apply: only push events on main may apply"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f bootstrap/config.yaml ]] || ! grep -Eq '^[[:space:]]*enabled:[[:space:]]*true[[:space:]]*$' bootstrap/config.yaml; then
|
||||||
|
echo "Skipping bootstrap apply: bootstrap/config.yaml is not enabled"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${KUBECONFIG_B64:-}" ]]; then
|
||||||
|
echo "KUBECONFIG_B64 secret is required to apply bootstrap/applicationset.yaml"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -fsSL "https://dl.k8s.io/release/$(curl -fsSL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
|
||||||
|
-o /usr/local/bin/kubectl
|
||||||
|
chmod +x /usr/local/bin/kubectl
|
||||||
|
|
||||||
|
mkdir -p "${HOME}/.kube"
|
||||||
|
printf '%s' "$KUBECONFIG_B64" | base64 -d > "${HOME}/.kube/config"
|
||||||
|
|
||||||
|
if ! kubectl config current-context >/dev/null 2>&1; then
|
||||||
|
echo "Skipping bootstrap apply: kubeconfig secret is not usable in this runner"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Apply both ApplicationSets (multi-document YAML)
|
||||||
|
kubectl apply -f bootstrap/applicationset.yaml
|
||||||
Executable
+87
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
mkdir -p .ci-schemas/argoproj.io .ci-schemas/traefik.io .ci-schemas/bitnami.com .ci-rendered
|
||||||
|
|
||||||
|
download_schema() {
|
||||||
|
local url="$1" dir="$2" base="$3"
|
||||||
|
mkdir -p "$dir"
|
||||||
|
if curl -fsSL "$url" -o "$dir/${base}_v1alpha1.json" 2>/dev/null; then
|
||||||
|
cp "$dir/${base}_v1alpha1.json" "$dir/${base}.json"
|
||||||
|
else
|
||||||
|
echo "Warning: could not download schema $url"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_yaml_syntax() {
|
||||||
|
echo "==> Validating YAML syntax"
|
||||||
|
find . -type f \( -name '*.yaml' -o -name '*.yml' \) \
|
||||||
|
! -path './.git/*' ! -path './.ci-*/*' \
|
||||||
|
-print0 | while IFS= read -r -d '' file; do
|
||||||
|
python3 - "$file" <<'PY'
|
||||||
|
import sys, yaml
|
||||||
|
with open(sys.argv[1]) as f:
|
||||||
|
list(yaml.safe_load_all(f))
|
||||||
|
PY
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_kustomize_overlays() {
|
||||||
|
echo "==> Building Kustomize overlays"
|
||||||
|
for overlay in manifest/overlays/*/; do
|
||||||
|
[[ -f "${overlay}kustomization.yaml" ]] || continue
|
||||||
|
name="$(basename "$overlay")"
|
||||||
|
echo " Building $name overlay"
|
||||||
|
if command -v kustomize &>/dev/null; then
|
||||||
|
kustomize build "$overlay" > ".ci-rendered/kustomize-${name}.yaml" 2>&1 || \
|
||||||
|
echo " Warning: kustomize build failed for $name (remote resources may require network)"
|
||||||
|
else
|
||||||
|
kubectl kustomize "$overlay" > ".ci-rendered/kustomize-${name}.yaml" 2>&1 || \
|
||||||
|
echo " Warning: kubectl kustomize failed for $name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_crd_schemas() {
|
||||||
|
echo "==> Preparing CRD schemas"
|
||||||
|
download_schema \
|
||||||
|
"https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/argoproj.io/applicationset_v1alpha1.json" \
|
||||||
|
".ci-schemas/argoproj.io" "applicationset"
|
||||||
|
download_schema \
|
||||||
|
"https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/traefik.io/ingressroute_v1alpha1.json" \
|
||||||
|
".ci-schemas/traefik.io" "ingressroute"
|
||||||
|
download_schema \
|
||||||
|
"https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/bitnami.com/sealedsecret_v1alpha1.json" \
|
||||||
|
".ci-schemas/bitnami.com" "sealedsecret"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_kubernetes_manifests() {
|
||||||
|
echo "==> Validating Kubernetes manifests with kubeconform"
|
||||||
|
local bootstrap_enabled=false
|
||||||
|
if grep -Eq '^[[:space:]]*enabled:[[:space:]]*true' bootstrap/config.yaml 2>/dev/null; then
|
||||||
|
bootstrap_enabled=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
find . -type f \( -name '*.yaml' -o -name '*.yml' \) \
|
||||||
|
! -path './.git/*' ! -path './.gitea/*' ! -path './.ci-*/*' \
|
||||||
|
! -name '*kustomization.yaml' ! -name 'config.yaml' \
|
||||||
|
! -name '*.plain.yaml' ! -name '*.example.yaml' \
|
||||||
|
$( [[ "$bootstrap_enabled" != "true" ]] && echo "! -name 'applicationset.yaml'" ) \
|
||||||
|
| sort | xargs kubeconform \
|
||||||
|
-strict \
|
||||||
|
-kubernetes-version 1.35.0 \
|
||||||
|
-schema-location default \
|
||||||
|
-schema-location 'https://git.olb42.com/olb042/kubeconform/raw/branch/main/crdSchemas/{{ .ResourceKind }}_{{ .ResourceAPIVersion }}.json' \
|
||||||
|
-ignore-missing-schemas \
|
||||||
|
-summary
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! command -v python3 &>/dev/null; then echo "python3 required"; exit 1; fi
|
||||||
|
if ! command -v kubeconform &>/dev/null; then echo "kubeconform required"; exit 1; fi
|
||||||
|
|
||||||
|
validate_yaml_syntax
|
||||||
|
prepare_crd_schemas
|
||||||
|
validate_kustomize_overlays
|
||||||
|
validate_kubernetes_manifests
|
||||||
|
|
||||||
|
echo "Validation completed"
|
||||||
Reference in New Issue
Block a user