feat: GKE production deployment — CNPG + GCS backups + Google Workspace SSO

Add complete GKE deployment stack:
- deploy/gcp/gke/: namespace, CloudNativePG cluster (premium-rwo, 100Gi),
  GCS barman-cloud backup with Workload Identity SA, ArgoCD OIDC ConfigMap,
  GitLab OmniAuth secret template, knoe-auth Google OIDC secret template
- etc/init_cnpg_gke.sh: provision GCS buckets, GCP SA, WI binding, CNPG operator
- etc/init_argocd.sh: apply argocd-oidc-cm.yaml when PLATFORM_DOMAIN/FRONTDOOR_HOST set
- etc/init_gitlab.sh: inject OmniAuth openid_connect block when FRONTDOOR_HOST set
- prole-auth-deployment.yaml: add GOOGLE_OIDC_ENABLED env vars (all optional)
- supabase/helm/prole-supabase/values-gke.yaml: GoTrue Google OAuth overlay
- knoe/core/prod_config.py: add backupProvider, workloadIdentitySA, googleClientId/Secret/HostedDomain

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chrisfu 2026-04-01 10:41:55 -07:00
parent 68e4a5418c
commit 620bdd25eb
13 changed files with 557 additions and 0 deletions

View File

@ -0,0 +1,58 @@
---
# Patch argocd-cm to enable Google Workspace SSO via knoe-auth OIDC issuer.
# Applied by etc/init_argocd.sh --mode k8s after ArgoCD is installed.
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
labels:
app.kubernetes.io/name: argocd-cm
app.kubernetes.io/part-of: argocd
data:
# Set url to your ArgoCD external hostname
url: "https://argocd.${PLATFORM_DOMAIN}"
oidc.config: |
name: "Google Workspace"
issuer: "https://${FRONTDOOR_HOST}/auth"
clientID: argocd
clientSecret: $oidc.argocd.clientSecret
requestedScopes:
- openid
- profile
- email
- groups
requestedIDTokenClaims:
groups:
essential: true
---
# RBAC: map Google Workspace admin email to ArgoCD admin role.
# Add additional group/email → role mappings under policy.csv.
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-rbac-cm
namespace: argocd
labels:
app.kubernetes.io/name: argocd-rbac-cm
app.kubernetes.io/part-of: argocd
data:
policy.default: role:readonly
policy.csv: |
g, ${BOOTSTRAP_ADMIN_EMAIL}, role:admin
---
# Secret referenced by oidc.config above ($oidc.argocd.clientSecret).
# Populated by the installer from the OIDC Client Secret field.
apiVersion: v1
kind: Secret
metadata:
name: argocd-secret
namespace: argocd
labels:
app.kubernetes.io/name: argocd-secret
app.kubernetes.io/part-of: argocd
type: Opaque
stringData:
oidc.argocd.clientSecret: "" # set by installer

View File

@ -0,0 +1,30 @@
apiVersion: v1
kind: Secret
metadata:
name: gitlab-google-oidc
namespace: gitlab
labels:
app.kubernetes.io/managed-by: prole-installer
# GitLab OmniAuth provider config for OpenID Connect via knoe-auth.
# See: https://docs.gitlab.com/ee/administration/auth/oidc.html
# Applied by etc/init_gitlab.sh --mode k8s
stringData:
provider: |
name: openid_connect
label: "Google Workspace"
icon: "https://www.google.com/favicon.ico"
args:
name: openid_connect
scope:
- openid
- profile
- email
response_type: code
issuer: "https://${FRONTDOOR_HOST}/auth"
client_auth_method: query
uid_field: email
send_scope_to_token_endpoint: false
client_options:
identifier: "" # set to OIDC client_id
secret: "" # set to OIDC client_secret
redirect_uri: "https://git.${PLATFORM_DOMAIN}/users/auth/openid_connect/callback"

View File

@ -0,0 +1,14 @@
apiVersion: v1
kind: Secret
metadata:
name: knoe-auth-google-oidc
namespace: knoe-system
labels:
app.kubernetes.io/managed-by: prole-installer
# Fill in client_id and client_secret from your Google Cloud Console OAuth 2.0 credentials.
# Create at: https://console.cloud.google.com/apis/credentials
# Authorized redirect URI: https://<frontdoor-host>/auth/callback/google
stringData:
client_id: ""
client_secret: ""
hosted_domain: "" # restrict to a Google Workspace domain, e.g. "yourdomain.com"; blank = any Google account

View File

@ -0,0 +1,27 @@
---
# Kubernetes ServiceAccount bound to the GCP service account via Workload Identity.
# The CNPG operator uses this SA to authenticate against GCS without a static key.
apiVersion: v1
kind: ServiceAccount
metadata:
name: cnpg-backup-sa
namespace: ecosystem-0
annotations:
# Set by etc/init_cnpg_gke.sh — substituted at deploy time
iam.gke.io/gcp-service-account: "cnpg-backup@${GCP_PROJECT_ID}.iam.gserviceaccount.com"
---
# Daily scheduled backup at 02:00 UTC.
# Backup data lands in gs://knoe-0-backups/ (WAL is streamed continuously by CNPG).
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: knoe-db-daily
namespace: ecosystem-0
spec:
schedule: "0 2 * * *"
backupOwnerReference: self
cluster:
name: knoe-db
method: barmanObjectStore
immediate: false

105
deploy/gcp/gke/knoe-db.yaml Normal file
View File

@ -0,0 +1,105 @@
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: knoe-db
namespace: ecosystem-0
spec:
instances: 3
# Image pulled from GCP Artifact Registry — set ARTIFACT_REGISTRY in conf/prod/gcp.cfg
# e.g. us-central1-docker.pkg.dev/<project>/prole/knoe-db:18-140
imageName: "${ARTIFACT_REGISTRY}/knoe-db:18-140"
postgresUID: 100
postgresGID: 101
maxSyncReplicas: 1
affinity:
enablePodAntiAffinity: true
podAntiAffinityType: preferred
topologyKey: topology.kubernetes.io/zone # GKE zone-aware spreading
postgresql:
parameters:
shared_buffers: 256MB
pg_stat_statements.max: '10000'
pg_stat_statements.track: all
shared_preload_libraries:
- pg_stat_statements
- pg_tde
pg_hba:
- local all postgres trust
- local all prole scram-sha-256
- host all postgres all scram-sha-256
- host prole knoe-db all scram-sha-256
- host all all all scram-sha-256
- hostssl prole knoe-db all scram-sha-256
bootstrap:
initdb:
database: knoe-db
owner: prole
localeCollate: 'en_US.utf8'
localeCType: 'en_US.utf8'
secret:
name: knoe-db-user
postInitTemplateSQL:
- CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
postInitSQL:
- CREATE EXTENSION IF NOT EXISTS pg_tde;
- CREATE EXTENSION IF NOT EXISTS pgcrypto;
- GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO prole;
- CREATE EXTENSION IF NOT EXISTS postgis;
- CREATE EXTENSION IF NOT EXISTS postgis_topology;
- CREATE EXTENSION IF NOT EXISTS vector;
- CREATE EXTENSION IF NOT EXISTS tds_fdw;
- GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO prole;
- GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA topology TO prole;
- CREATE SCHEMA IF NOT EXISTS storage;
- CREATE SCHEMA IF NOT EXISTS graphql_public;
- CREATE ROLE anon NOLOGIN;
- CREATE ROLE authenticator LOGIN;
- GRANT USAGE ON SCHEMA public TO anon;
- GRANT USAGE ON SCHEMA storage TO anon;
- GRANT USAGE ON SCHEMA graphql_public TO anon;
- GRANT anon TO authenticator;
certificates:
serverTLSSecret: knoe-db-tls
serverCASecret: knoe-db-ca
enableSuperuserAccess: true
# GCS backup via Workload Identity — see knoe-db-backup-gcs.yaml
backup:
barmanObjectStore:
destinationPath: "gs://knoe-0-backups/"
googleCredentials:
applicationCredentials:
name: "" # empty = Workload Identity; no static key needed
wal:
compression: gzip
data:
compression: gzip
retentionPolicy: "14d"
storage:
size: 100Gi
pvcTemplate:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
storageClassName: premium-rwo # GCP SSD persistent disk (pd-ssd)
walStorage:
size: 20Gi
pvcTemplate:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
storageClassName: premium-rwo
monitoring:
enablePodMonitor: true # GKE managed Prometheus

View File

@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: ecosystem-0
labels:
app.kubernetes.io/managed-by: prole-installer

View File

@ -0,0 +1,26 @@
# Workload Identity binding documentation.
#
# This file is NOT applied with kubectl — it documents the gcloud commands
# executed by etc/init_cnpg_gke.sh to establish the Workload Identity link
# between the Kubernetes ServiceAccount (cnpg-backup-sa) and the GCP
# service account (cnpg-backup@<project>.iam.gserviceaccount.com).
#
# Commands run by init_cnpg_gke.sh:
#
# gcloud iam service-accounts create cnpg-backup \
# --display-name="CNPG GCS Backup" \
# --project=$GCP_PROJECT_ID
#
# gsutil iam ch \
# serviceAccount:cnpg-backup@$GCP_PROJECT_ID.iam.gserviceaccount.com:objectAdmin \
# gs://knoe-0-backups
#
# gcloud iam service-accounts add-iam-policy-binding \
# cnpg-backup@$GCP_PROJECT_ID.iam.gserviceaccount.com \
# --role=roles/iam.workloadIdentityUser \
# --member="serviceAccount:$GCP_PROJECT_ID.svc.id.goog[ecosystem-0/cnpg-backup-sa]"
#
# kubectl annotate serviceaccount cnpg-backup-sa \
# -n ecosystem-0 \
# iam.gke.io/gcp-service-account=cnpg-backup@$GCP_PROJECT_ID.iam.gserviceaccount.com \
# --overwrite

View File

@ -155,6 +155,37 @@ spec:
configMapKeyRef:
name: prole-auth-kerberos
key: realm
# ── Google Workspace OIDC (primary auth for GKE/prod) ──────────
# Secret created by installer from Auth+Routing tab OIDC fields.
# Template: deploy/gcp/gke/knoe-auth-google-oidc-secret.example.yaml
- name: GOOGLE_OIDC_ENABLED
value: "true"
- name: GOOGLE_CLIENT_ID
valueFrom:
secretKeyRef:
name: knoe-auth-google-oidc
key: client_id
optional: true # Kerberos-only deployments can omit this secret
- name: GOOGLE_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: knoe-auth-google-oidc
key: client_secret
optional: true
- name: GOOGLE_HOSTED_DOMAIN
valueFrom:
secretKeyRef:
name: knoe-auth-google-oidc
key: hosted_domain
optional: true # blank = any Google account; set to restrict to Workspace domain
- name: OIDC_ISSUER_URL
value: "https://knoe-auth.knoe-system.svc.cluster.local:8080"
- name: OIDC_BASE_URL
valueFrom:
configMapKeyRef:
name: prole-platform-config
key: frontdoorHost
optional: true
volumeMounts:
- name: keytab
mountPath: /etc/prole/keytabs

View File

@ -32,6 +32,9 @@ fi
ACTION="$COMMON_CORE_ACTION"
REPO_ROOT="${PROLE_HOME:-$(cd "$SCRIPT_DIR/.." && pwd)}"
GKE_MANIFEST_DIR="${GKE_MANIFEST_DIR:-$REPO_ROOT/deploy/gcp/gke}"
if [[ -d "$SCRIPT_DIR/../k8s/argocd" ]]; then
ARGOCD_MANIFEST_DIR="$SCRIPT_DIR/../k8s/argocd"
elif [[ -n "${PROLE_HOME:-}" && -d "$PROLE_HOME/k8s/argocd" ]]; then
@ -52,6 +55,11 @@ ARGOCD_PORT_FORWARD_LOCAL=${ARGOCD_PORT_FORWARD_LOCAL:-8081}
ARGOCD_PORT_FORWARD_REMOTE=${ARGOCD_PORT_FORWARD_REMOTE:-80}
ARGOCD_NODE_SELECTOR=${ARGOCD_NODE_SELECTOR:-}
# ── Google Workspace OIDC (applied when all three vars are set) ───────────────
PLATFORM_DOMAIN="${PLATFORM_DOMAIN:-}"
FRONTDOOR_HOST="${FRONTDOOR_HOST:-}"
BOOTSTRAP_ADMIN_EMAIL="${BOOTSTRAP_ADMIN_EMAIL:-}"
ensure_tools() {
command -v kubectl >/dev/null || { echo "Missing required tool: kubectl" >&2; exit 1; }
}
@ -122,6 +130,24 @@ apply_argocd() {
kubectl rollout status statefulset/argocd-application-controller -n "$ARGOCD_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
}
apply_argocd_oidc_config() {
local cm_file="$GKE_MANIFEST_DIR/argocd-oidc-cm.yaml"
if [[ -z "$PLATFORM_DOMAIN" || -z "$FRONTDOOR_HOST" || -z "$BOOTSTRAP_ADMIN_EMAIL" ]]; then
return 0
fi
if [[ ! -f "$cm_file" ]]; then
echo "WARN: ArgoCD OIDC config not found at $cm_file; skipping OIDC patch." >&2
return 0
fi
echo "Applying ArgoCD OIDC config (issuer=https://${FRONTDOOR_HOST}/auth) ..."
PLATFORM_DOMAIN="$PLATFORM_DOMAIN" \
FRONTDOOR_HOST="$FRONTDOOR_HOST" \
BOOTSTRAP_ADMIN_EMAIL="$BOOTSTRAP_ADMIN_EMAIL" \
envsubst < "$cm_file" \
| kubectl apply --server-side --force-conflicts \
--field-manager=prole-installer -f -
}
rollout_restart_argocd() {
kubectl -n "$ARGOCD_NAMESPACE" rollout restart deploy/argocd-server deploy/argocd-repo-server \
deploy/argocd-dex-server deploy/argocd-applicationset-controller deploy/argocd-notifications-controller \
@ -144,6 +170,7 @@ case "$ACTION" in
ensure_tools
ensure_namespace
apply_argocd
apply_argocd_oidc_config
prole_register_port_forward "argocd" "$ARGOCD_NAMESPACE" "svc/${ARGOCD_SERVER_SERVICE}" \
"$ARGOCD_PORT_FORWARD_LOCAL" "$ARGOCD_PORT_FORWARD_REMOTE" "0.0.0.0" "TCP" "ArgoCD"
;;
@ -152,6 +179,7 @@ case "$ACTION" in
ensure_namespace
apply_argocd
rollout_restart_argocd
apply_argocd_oidc_config
prole_register_port_forward "argocd" "$ARGOCD_NAMESPACE" "svc/${ARGOCD_SERVER_SERVICE}" \
"$ARGOCD_PORT_FORWARD_LOCAL" "$ARGOCD_PORT_FORWARD_REMOTE" "0.0.0.0" "TCP" "ArgoCD"
;;

138
etc/init_cnpg_gke.sh Executable file
View File

@ -0,0 +1,138 @@
#!/usr/bin/env bash
# init_cnpg_gke.sh
# Provision CloudNativePG on GKE with GCS backup via Workload Identity.
#
# Usage:
# ./etc/init_cnpg_gke.sh [--project PROJECT_ID] [--region REGION] [--cluster CLUSTER_NAME]
#
# Env vars (override flags):
# GCP_PROJECT_ID, GCP_REGION, GKE_CLUSTER, GCS_BACKUP_BUCKET,
# CNPG_NAMESPACE, ARTIFACT_REGISTRY, CLOUDSDK_AUTH_ACCESS_TOKEN
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
GKE_MANIFEST_DIR="$REPO_ROOT/deploy/gcp/gke"
# ── Defaults ────────────────────────────────────────────────────────────────
GCP_PROJECT_ID="${GCP_PROJECT_ID:-}"
GCP_REGION="${GCP_REGION:-us-central1}"
GKE_CLUSTER="${GKE_CLUSTER:-knoe-dev-0}"
GCS_BACKUP_BUCKET="${GCS_BACKUP_BUCKET:-knoe-0-backups}"
GCS_WAL_BUCKET="${GCS_WAL_BUCKET:-knoe-0-wal}"
CNPG_NAMESPACE="${CNPG_NAMESPACE:-ecosystem-0}"
CNPG_BACKUP_SA="${CNPG_BACKUP_SA:-cnpg-backup}"
ARTIFACT_REGISTRY="${ARTIFACT_REGISTRY:-}"
# ── Argument parsing ─────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--project) GCP_PROJECT_ID="$2"; shift 2 ;;
--region) GCP_REGION="$2"; shift 2 ;;
--cluster) GKE_CLUSTER="$2"; shift 2 ;;
--bucket) GCS_BACKUP_BUCKET="$2"; shift 2 ;;
--namespace) CNPG_NAMESPACE="$2"; shift 2 ;;
--registry) ARTIFACT_REGISTRY="$2"; shift 2 ;;
*) echo "Unknown argument: $1" >&2; exit 1 ;;
esac
done
[[ -z "$GCP_PROJECT_ID" ]] && {
# Try to read from gcp.cfg
GCP_CFG="$REPO_ROOT/conf/prod/gcp.cfg"
if [[ -f "$GCP_CFG" ]]; then
GCP_PROJECT_ID=$(grep '^project_id' "$GCP_CFG" | sed 's/.*=\s*"\?\([^"]*\)"\?.*/\1/' | tr -d '[:space:]')
fi
}
[[ -z "$GCP_PROJECT_ID" ]] && { echo "ERROR: GCP_PROJECT_ID not set and conf/prod/gcp.cfg not found." >&2; exit 1; }
GCP_SA_EMAIL="${CNPG_BACKUP_SA}@${GCP_PROJECT_ID}.iam.gserviceaccount.com"
log() { echo "[init_cnpg_gke] $*"; }
# ── Tool checks ──────────────────────────────────────────────────────────────
for tool in gcloud kubectl gsutil; do
command -v "$tool" >/dev/null || { echo "ERROR: $tool not found in PATH." >&2; exit 1; }
done
# ── 1. Acquire kubeconfig ────────────────────────────────────────────────────
log "Fetching kubeconfig for $GKE_CLUSTER in $GCP_REGION ..."
gcloud container clusters get-credentials "$GKE_CLUSTER" \
--project "$GCP_PROJECT_ID" \
--region "$GCP_REGION"
# ── 2. Create GCS buckets ────────────────────────────────────────────────────
for bucket in "$GCS_BACKUP_BUCKET" "$GCS_WAL_BUCKET"; do
if gsutil ls -b "gs://$bucket" >/dev/null 2>&1; then
log "Bucket gs://$bucket already exists."
else
log "Creating bucket gs://$bucket ..."
gsutil mb -p "$GCP_PROJECT_ID" -l "$GCP_REGION" "gs://$bucket"
gsutil versioning set on "gs://$bucket"
gsutil lifecycle set /dev/stdin "gs://$bucket" <<EOF
{"rule":[{"action":{"type":"Delete"},"condition":{"age":30}}]}
EOF
fi
done
# ── 3. Create GCP service account ────────────────────────────────────────────
if gcloud iam service-accounts describe "$GCP_SA_EMAIL" --project "$GCP_PROJECT_ID" >/dev/null 2>&1; then
log "Service account $GCP_SA_EMAIL already exists."
else
log "Creating service account $GCP_SA_EMAIL ..."
gcloud iam service-accounts create "$CNPG_BACKUP_SA" \
--display-name="CNPG GCS Backup" \
--project="$GCP_PROJECT_ID"
fi
# ── 4. Grant storage access ───────────────────────────────────────────────────
log "Granting objectAdmin on backup buckets to $GCP_SA_EMAIL ..."
for bucket in "$GCS_BACKUP_BUCKET" "$GCS_WAL_BUCKET"; do
gsutil iam ch \
"serviceAccount:${GCP_SA_EMAIL}:objectAdmin" \
"gs://$bucket"
done
# ── 5. Bind Workload Identity ─────────────────────────────────────────────────
log "Binding Workload Identity ..."
gcloud iam service-accounts add-iam-policy-binding "$GCP_SA_EMAIL" \
--role=roles/iam.workloadIdentityUser \
--member="serviceAccount:${GCP_PROJECT_ID}.svc.id.goog[${CNPG_NAMESPACE}/cnpg-backup-sa]" \
--project="$GCP_PROJECT_ID"
# ── 6. Apply namespace ────────────────────────────────────────────────────────
log "Applying namespace $CNPG_NAMESPACE ..."
kubectl apply -f "$GKE_MANIFEST_DIR/namespace.yaml"
# ── 7. Install CNPG operator (if not present) ─────────────────────────────────
if ! kubectl get crd clusters.postgresql.cnpg.io >/dev/null 2>&1; then
log "Installing CloudNativePG operator ..."
kubectl apply --server-side -f \
https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/main/releases/cnpg-1.24.0.yaml
log "Waiting for CNPG operator to be ready ..."
kubectl rollout status deployment/cnpg-controller-manager -n cnpg-system --timeout=120s
else
log "CNPG operator already installed."
fi
# ── 8. Apply ServiceAccount + annotate with WI ───────────────────────────────
log "Applying CNPG backup ServiceAccount with Workload Identity annotation ..."
GCP_PROJECT_ID="$GCP_PROJECT_ID" envsubst < "$GKE_MANIFEST_DIR/knoe-db-backup-gcs.yaml" \
| kubectl apply -f -
kubectl annotate serviceaccount cnpg-backup-sa \
-n "$CNPG_NAMESPACE" \
"iam.gke.io/gcp-service-account=${GCP_SA_EMAIL}" \
--overwrite
# ── 9. Apply CNPG cluster ────────────────────────────────────────────────────
log "Applying knoe-db CNPG cluster ..."
ARTIFACT_REGISTRY="${ARTIFACT_REGISTRY}" envsubst < "$GKE_MANIFEST_DIR/knoe-db.yaml" \
| kubectl apply -f -
log ""
log "Done. Monitor with:"
log " kubectl -n $CNPG_NAMESPACE get cluster knoe-db"
log " kubectl -n $CNPG_NAMESPACE get pods -l cnpg.io/cluster=knoe-db"
log " kubectl -n $CNPG_NAMESPACE get scheduledbackup"

View File

@ -96,6 +96,9 @@ DB_PORT="${GITLAB_DB_PORT:-5432}"
# GitLab domain (reuses the git.prole.org shared hostname)
GITLAB_DOMAIN="${GITLAB_DOMAIN:-git.prole.org}"
# Google Workspace OIDC via knoe-auth — set FRONTDOOR_HOST to enable
FRONTDOOR_HOST="${FRONTDOOR_HOST:-}"
GITLAB_RELEASE="gitlab"
# ---------------------------------------------------------------------------
@ -320,6 +323,22 @@ fi
# ---------------------------------------------------------------------------
# Create the GitLab CR (operator reconciles this into the full deployment)
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Apply gitlab-google-oidc secret (OIDC provider config for OmniAuth)
# ---------------------------------------------------------------------------
if [[ -n "$FRONTDOOR_HOST" ]]; then
PLATFORM_DOMAIN="${PLATFORM_DOMAIN:-}"
oidc_secret_tmpl="${SCRIPT_DIR}/../deploy/gcp/gke/gitlab-google-oidc-secret.example.yaml"
if [[ -f "$oidc_secret_tmpl" ]]; then
log "Applying gitlab-google-oidc secret (issuer=https://${FRONTDOOR_HOST}/auth) ..."
FRONTDOOR_HOST="$FRONTDOOR_HOST" PLATFORM_DOMAIN="$PLATFORM_DOMAIN" \
envsubst < "$oidc_secret_tmpl" | kubectl apply -n "$NAMESPACE" -f - >/dev/null || \
warn "Could not apply gitlab-google-oidc secret; create it manually from deploy/gcp/gke/gitlab-google-oidc-secret.example.yaml"
else
warn "gitlab-google-oidc-secret.example.yaml not found; skipping OIDC secret (create it manually)."
fi
fi
log "Applying GitLab CR (domain=${GITLAB_DOMAIN}, db=${GITLAB_DB_NAME}@${DB_HOST})..."
kubectl apply -f - <<EOF
@ -356,6 +375,20 @@ spec:
# the gitlab namespace but can reference services in knoe-db.
serviceAccount:
create: true
$(if [[ -n "$FRONTDOOR_HOST" ]]; then
cat <<OMNIAUTH
appConfig:
omniauth:
enabled: true
autoSignInWithProvider: openid_connect
allowSingleSignOn:
- openid_connect
blockAutoCreatedUsers: false
providers:
- secret: gitlab-google-oidc
key: provider
OMNIAUTH
fi)
# Disable bundled sub-charts — we provide our own data stores
postgresql:
install: false

View File

@ -49,6 +49,8 @@ class ProdBackupsConfig:
backupBucket: str = "knoe-0-backups"
walBucket: str = "knoe-0-wal"
retentionDays: int = 14
backupProvider: str = "gcs" # "gcs" | "garage"
workloadIdentitySA: str = "cnpg-backup"
@dataclass(slots=True)
@ -58,6 +60,9 @@ class ProdAuthConfig:
clientId: str = "secretref://google-oidc-client-id"
clientSecret: str = "secretref://google-oidc-client-secret"
bootstrapAdminEmail: str = "admin@knoey.com"
googleClientId: str = "" # transient — not persisted to prole.cfg
googleClientSecret: str = "" # transient — not persisted to prole.cfg
googleHostedDomain: str = "" # blank = any Google account; set to restrict Workspace domain
@dataclass(slots=True)
@ -212,6 +217,10 @@ def build_config(payload: dict[str, Any] | None = None) -> KnoeProductionConfig:
cfg.backups.retentionDays = _coerce_int(
backups.get("retentionDays", cfg.backups.retentionDays), cfg.backups.retentionDays
)
cfg.backups.backupProvider = _str_or_empty(backups.get("backupProvider")) or cfg.backups.backupProvider
cfg.backups.workloadIdentitySA = (
_str_or_empty(backups.get("workloadIdentitySA")) or cfg.backups.workloadIdentitySA
)
cfg.auth.provider = _str_or_empty(auth.get("provider")) or cfg.auth.provider
cfg.auth.issuer = _str_or_empty(auth.get("issuer")) or cfg.auth.issuer
@ -220,6 +229,9 @@ def build_config(payload: dict[str, Any] | None = None) -> KnoeProductionConfig:
cfg.auth.bootstrapAdminEmail = (
_str_or_empty(auth.get("bootstrapAdminEmail")) or cfg.auth.bootstrapAdminEmail
)
cfg.auth.googleClientId = _str_or_empty(auth.get("googleClientId"))
cfg.auth.googleClientSecret = _str_or_empty(auth.get("googleClientSecret"))
cfg.auth.googleHostedDomain = _str_or_empty(auth.get("googleHostedDomain"))
cfg.routing.frontdoorHost = _str_or_empty(routing.get("frontdoorHost")) or cfg.routing.frontdoorHost
cfg.routing.platformDomain = _str_or_empty(routing.get("platformDomain")) or cfg.routing.platformDomain
@ -354,10 +366,13 @@ def opentofu_vars(config: KnoeProductionConfig) -> dict[str, Any]:
"backup_bucket": config.backups.backupBucket,
"wal_bucket": config.backups.walBucket,
"backup_retention_days": config.backups.retentionDays,
"backup_provider": config.backups.backupProvider,
"workload_identity_sa": config.backups.workloadIdentitySA,
"oidc_issuer": config.auth.issuer,
"oidc_client_id_ref": config.auth.clientId,
"oidc_client_secret_ref": config.auth.clientSecret,
"bootstrap_admin_email": config.auth.bootstrapAdminEmail,
"google_hosted_domain": config.auth.googleHostedDomain,
"frontdoor_host": config.routing.frontdoorHost,
"platform_domain": config.routing.platformDomain,
"tls_mode": config.routing.tlsMode,

View File

@ -0,0 +1,46 @@
# GKE production values overlay for prole-supabase Helm chart.
#
# Apply on top of values.yaml:
# helm upgrade --install prole-supabase ./supabase/helm/prole-supabase \
# --values supabase/helm/prole-supabase/values.yaml \
# --values supabase/helm/prole-supabase/values-gke.yaml \
# --set environment.auth.GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID="$CLIENT_ID" \
# --set environment.auth.GOTRUE_EXTERNAL_GOOGLE_SECRET="$CLIENT_SECRET" \
# --set environment.auth.GOTRUE_SITE_URL="https://$FRONTDOOR_HOST" \
# --set environment.auth.GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI="https://$FRONTDOOR_HOST/auth/v1/callback" \
# --set environment.auth.GOTRUE_URI_ALLOW_LIST="https://$FRONTDOOR_HOST/**"
#
# Or set FRONTDOOR_HOST, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET in environment
# and let init_supabase.sh --mode k8s substitute them automatically.
# ── Storage: use GCS instead of bundled MinIO ───────────────────────────────
# When CNPG Workload Identity is active the pod SA already has storage access;
# set storage driver to gcs and disable minio.
deployment:
minio:
enabled: false
# ── GoTrue: enable Google OAuth via knoe-auth OIDC broker ───────────────────
environment:
auth:
# Override base URL to the GKE frontdoor
API_EXTERNAL_URL: "" # set to https://<frontdoor-host>/auth/v1 at deploy time
GOTRUE_SITE_URL: "" # set to https://<frontdoor-host>
GOTRUE_URI_ALLOW_LIST: "" # set to https://<frontdoor-host>/**
# Google OAuth2 — client ID and secret from knoe-auth-google-oidc secret
GOTRUE_EXTERNAL_GOOGLE_ENABLED: "true"
GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: "" # injected at deploy time
GOTRUE_EXTERNAL_GOOGLE_SECRET: "" # injected at deploy time
GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI: "" # set to https://<frontdoor-host>/auth/v1/callback
# Disable anonymous sign-in in production
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: "false"
# Auto-confirm email (Google accounts are pre-verified)
GOTRUE_MAILER_AUTOCONFIRM: "true"
# ── Ingress: use GKE-managed ingress class ──────────────────────────────────
ingress:
studio:
className: "gce" # GKE L7 load balancer; override with "kong" if Kong is installed