feat(mock_val): rewrite init scripts; add new service init scripts

Rewrites (updated for knoe namespace, GKE support, and current service configs):
  init_gitlab.sh, init_kong.sh, init_cnpg_backup.sh, init_monitoring.sh,
  init_garage_store.sh, init_gitea.sh, init_kdc.sh, init_openbao.sh,
  init_argocd.sh, init_certmgr.sh, init_common_services.sh, init_db_manager.sh,
  init_forgejo.sh, init_k3s_registry.sh, init_kerberos.sh, init_nginx_ingress.sh,
  init_port_forwards.sh, init_registry.sh, init_service_layer.sh

Deleted: init_cloudnative_pg.sh (superseded by init_cnpg_gke.sh)

New scripts:
  init_cnpg_gke.sh      — CNPG setup for GKE with Workload Identity
  init_knoe_auth.sh     — knoe-auth OIDC service init
  init_knoe_users.sh    — user provisioning
  init_redis.sh         — Redis init
  init_oauth2_proxy.sh / init_oauth2_proxy_prole.sh — OAuth2 proxy setup
  init_grafana_oauth.sh / init_grafana_oauth_prole.sh — Grafana OAuth wiring
  init_1password.sh     — 1Password Connect init
  init_min.sh           — minimal bootstrap

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
chrisfu 2026-05-23 21:31:27 -07:00
parent 11064cbd5b
commit 3f96f66a78
30 changed files with 10317 additions and 3082 deletions

79
mock_val/init_1password.sh Executable file
View File

@ -0,0 +1,79 @@
#!/usr/bin/env bash
# init_1password.sh
# Preflight: sign in to 1Password, create the 'knoey' vault if absent,
# and ensure the 'administrator' item (DB master password) exists.
#
# Called by install.sh before the Python installer. Skipped in --min mode.
set -euo pipefail
# ── helpers ────────────────────────────────────────────────────────────────
_info() { echo "==> [1Password] $*"; }
_warn() { echo " [WARN] $*" >&2; }
_fatal() { echo " [ERROR] $*" >&2; exit 1; }
VAULT="knoey"
ADMIN_ITEM="administrator"
# ── skip in --min mode ─────────────────────────────────────────────────────
for arg in "$@"; do
if [[ "$arg" == "--min" ]]; then
_warn "Skipping 1Password preflight in --min mode."
exit 0
fi
done
# ── require op CLI ─────────────────────────────────────────────────────────
if ! command -v op >/dev/null 2>&1; then
_fatal "1Password CLI (op) not found.
Install: brew install 1password-cli
Docs: https://developer.1password.com/docs/cli"
fi
_info "op CLI found: $(op --version)"
# ── sign in ────────────────────────────────────────────────────────────────
if ! op whoami >/dev/null 2>&1; then
# If running non-interactively (no TTY), skip rather than hang.
if [[ ! -t 0 ]]; then
_warn "No active 1Password session and no TTY — skipping 1Password preflight."
exit 0
fi
_info "No active 1Password session. Signing in..."
op signin
fi
_info "Signed in as: $(op whoami --format json 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('email','unknown'))" 2>/dev/null || op whoami)"
# ── create knoey vault if absent ───────────────────────────────────────────
if op vault get "$VAULT" >/dev/null 2>&1; then
_info "Vault '$VAULT' already exists."
else
_info "Creating vault '$VAULT'..."
op vault create "$VAULT"
_info "Vault '$VAULT' created."
fi
# ── ensure administrator item exists ───────────────────────────────────────
if op item get "$ADMIN_ITEM" --vault "$VAULT" >/dev/null 2>&1; then
_info "Item '$ADMIN_ITEM' already exists in vault '$VAULT'."
else
_info "Creating item '$ADMIN_ITEM' in vault '$VAULT' with a generated password..."
op item create \
--category login \
--title "$ADMIN_ITEM" \
--vault "$VAULT" \
--generate-password="32,letters,digits"
_info "Item '$ADMIN_ITEM' created."
fi
# ── export for child processes ─────────────────────────────────────────────
export OP_VAULT="$VAULT"
_info "OP_VAULT=$OP_VAULT"

View File

@ -32,6 +32,9 @@ fi
ACTION="$COMMON_CORE_ACTION"
REPO_ROOT="${KNOE_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 "${KNOE_HOME:-}" && -d "$KNOE_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=knoe-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
knoe_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
knoe_register_port_forward "argocd" "$ARGOCD_NAMESPACE" "svc/${ARGOCD_SERVER_SERVICE}" \
"$ARGOCD_PORT_FORWARD_LOCAL" "$ARGOCD_PORT_FORWARD_REMOTE" "0.0.0.0" "TCP" "ArgoCD"
;;

View File

@ -18,10 +18,13 @@ _has_config=0
for _arg in "$@"; do
[[ "$_arg" == "-c" || "$_arg" == "--config" || "$_arg" == -c=* || "$_arg" == --config=* ]] && _has_config=1
done
if [[ $_has_config -eq 0 && -f "$SCRIPT_DIR/../conf/knoe.cfg" ]]; then
set -- "-c" "$SCRIPT_DIR/../conf/knoe.cfg" "$@"
if [[ $_has_config -eq 0 ]]; then
_default_cfg="$(common_core_default_config_path "$SCRIPT_DIR" || true)"
if [[ -n "$_default_cfg" ]]; then
set -- "-c" "$_default_cfg" "$@"
fi
fi
unset _has_config _arg
unset _has_config _arg _default_cfg
common_core_preparse_config "$@"
@ -61,7 +64,7 @@ fi
usage() {
cat <<USAGE
Usage: $0 [--mode MODE] [-n NAMESPACE] [${COMMON_CORE_ACTIONS//|/|}] [-c conf/knoe.cfg]
Usage: $0 [--mode MODE] [-n NAMESPACE] [${COMMON_CORE_ACTIONS//|/|}] [-c conf/{k3d|k3s|gke}.cfg]
Actions:
start Install cert-manager (CRDs + controller)

File diff suppressed because it is too large Load Diff

View File

@ -24,30 +24,28 @@ fi
ACTION=${1:-start}
NAMESPACE="${PROLE_NAMESPACE}"
NAMESPACE="${PROLE_NAMESPACE:-${DATABASE_NAMESPACE:-${SERVICE_NAMESPACE:-knoe-db}}}"
CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-knoe-db}
CNPG_OPERATOR_NAMESPACE=${CNPG_OPERATOR_NAMESPACE:-cnpg-system}
GARAGE_NAME=${GARAGE_NAME:-garage}
SERVICE_NAMESPACE=${SERVICE_NAMESPACE:-}
if [[ -z "${GARAGE_NAMESPACE:-}" ]]; then
if [[ -n "$SERVICE_NAMESPACE" ]]; then
GARAGE_NAMESPACE="$SERVICE_NAMESPACE"
else
GARAGE_NAMESPACE="$NAMESPACE"
fi
fi
GARAGE_NAMESPACE=${GARAGE_NAMESPACE:-}
GARAGE_BACKUP_BUCKET=${GARAGE_BACKUP_BUCKET:-knoe-db-backups}
GARAGE_BACKUP_KEY_NAME=${GARAGE_BACKUP_KEY_NAME:-knoe-db-backup}
GARAGE_BACKUP_SECRET_NAME=${GARAGE_BACKUP_SECRET_NAME:-knoe-db-barman-s3}
GARAGE_S3_ENDPOINT=${GARAGE_S3_ENDPOINT:-http://$GARAGE_NAME.$GARAGE_NAMESPACE.svc.cluster.local:3900}
GARAGE_S3_ENDPOINT=${GARAGE_S3_ENDPOINT:-}
GARAGE_S3_REGION=${GARAGE_S3_REGION:-garage}
RUN_FIRST_BACKUP=${RUN_FIRST_BACKUP:-1}
RETENTION_POLICY=${RETENTION_POLICY:-30d}
GARAGE_LAYOUT_BOOTSTRAP_ENABLED=${GARAGE_LAYOUT_BOOTSTRAP_ENABLED:-1}
BARMAN_PLUGIN_NAME=${BARMAN_PLUGIN_NAME:-barman-cloud.cloudnative-pg.io}
BARMAN_OBJECT_NAME=${BARMAN_OBJECT_NAME:-knoe-db-barman-objectstore}
BACKUP_STATUS_TIMEOUT=${BACKUP_STATUS_TIMEOUT:-600}
BACKUP_STATUS_INTERVAL=${BACKUP_STATUS_INTERVAL:-10}
PLUGIN_READY_TIMEOUT=${PLUGIN_READY_TIMEOUT:-180}
OBJECTSTORE_READY_TIMEOUT=${OBJECTSTORE_READY_TIMEOUT:-180}
OBJECTSTORE_READY_INTERVAL=${OBJECTSTORE_READY_INTERVAL:-5}
KUBECTL_CONTEXT_OVERRIDE=${KUBECTL_CONTEXT_OVERRIDE:-${DB_CLUSTER_KUBECONTEXT:-${KUBECTL_CONTEXT:-${KUBECONTEXT:-}}}}
SCHEDULED_BACKUP_NAME=${SCHEDULED_BACKUP_NAME:-knoe-db-scheduled-backup}
SCHEDULED_BACKUP_CRON=${SCHEDULED_BACKUP_CRON:-"0 3 * * *"}
@ -76,6 +74,19 @@ ensure_tools() {
done
}
select_kube_context() {
local target_context="${KUBECTL_CONTEXT_OVERRIDE:-}"
if [[ -z "$target_context" ]]; then
return 0
fi
if kubectl config get-contexts "$target_context" >/dev/null 2>&1; then
kubectl config use-context "$target_context" >/dev/null
else
echo "WARN: Requested kubecontext '$target_context' not found; continuing with current context." >&2
fi
}
_kubectl_is_transient_error() {
local msg="${1:-}"
printf '%s' "$msg" | grep -Eqi \
@ -179,6 +190,34 @@ barman_crd_ready() {
kubectl get crd objectstores.barmancloud.cnpg.io >/dev/null 2>&1
}
garage_pod_exists_in_namespace() {
local ns="${1:-}"
local pod_name
[[ -z "$ns" ]] && return 1
pod_name=$(kubectl get pods -n "$ns" -l "app=$GARAGE_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
[[ -n "$pod_name" ]]
}
resolve_garage_namespace_and_endpoint() {
if [[ -z "$GARAGE_NAMESPACE" ]]; then
if [[ -n "$SERVICE_NAMESPACE" ]] && garage_pod_exists_in_namespace "$SERVICE_NAMESPACE"; then
GARAGE_NAMESPACE="$SERVICE_NAMESPACE"
elif garage_pod_exists_in_namespace "knoe-system"; then
GARAGE_NAMESPACE="knoe-system"
elif garage_pod_exists_in_namespace "$NAMESPACE"; then
GARAGE_NAMESPACE="$NAMESPACE"
elif [[ -n "$SERVICE_NAMESPACE" ]]; then
GARAGE_NAMESPACE="$SERVICE_NAMESPACE"
else
GARAGE_NAMESPACE="$NAMESPACE"
fi
fi
if [[ -z "$GARAGE_S3_ENDPOINT" ]]; then
GARAGE_S3_ENDPOINT="http://$GARAGE_NAME.$GARAGE_NAMESPACE.svc.cluster.local:3900"
fi
}
get_garage_pod() {
kubectl get pods -n "$GARAGE_NAMESPACE" -l "app=$GARAGE_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true
}
@ -193,6 +232,29 @@ garage_exec() {
kubectl exec -n "$GARAGE_NAMESPACE" "$pod" -- /garage "$@"
}
bootstrap_garage_layout_if_needed() {
if [[ "${GARAGE_LAYOUT_BOOTSTRAP_ENABLED}" != "1" ]]; then
return 0
fi
local init_script="$SCRIPT_DIR/init_garage_store.sh"
if [[ ! -f "$init_script" ]]; then
echo "WARN: Garage bootstrap helper not found: $init_script" >&2
return 0
fi
echo "Garage layout still not applied; invoking Garage bootstrap recovery ..."
if [[ -n "${KNOE_MODE:-}" ]]; then
if ! bash "$init_script" --mode "$KNOE_MODE" start; then
echo "WARN: Garage bootstrap recovery failed (continuing wait loop)." >&2
fi
else
if ! bash "$init_script" start; then
echo "WARN: Garage bootstrap recovery failed (continuing wait loop)." >&2
fi
fi
}
parse_key_output() {
local output="$1"
local access_key secret_key
@ -207,6 +269,7 @@ parse_key_output() {
ensure_garage_ready() {
echo "Checking Garage readiness ..."
local i status_out
local bootstrap_attempted=0
for i in {1..30}; do
if status_out=$(garage_exec status 2>/dev/null); then
# If layout is applied, DataAvail should eventually show something or at least the node should be healthy.
@ -219,6 +282,10 @@ ensure_garage_ready() {
return 0
fi
fi
if [[ "$bootstrap_attempted" -eq 0 && "$i" -ge 6 ]]; then
bootstrap_garage_layout_if_needed
bootstrap_attempted=1
fi
echo "Waiting for Garage layout to be applied... ($i/30)"
sleep 5
done
@ -292,13 +359,88 @@ spec:
OBJECTSTORE
}
objectstore_spec_matches_expected() {
local destination endpoint retention
local access_key_secret access_key_name secret_key_secret secret_key_name
local region_secret region_name
destination=$(kubectl -n "$NAMESPACE" get objectstore "$BARMAN_OBJECT_NAME" -o jsonpath='{.spec.configuration.destinationPath}' 2>/dev/null || true)
endpoint=$(kubectl -n "$NAMESPACE" get objectstore "$BARMAN_OBJECT_NAME" -o jsonpath='{.spec.configuration.endpointURL}' 2>/dev/null || true)
retention=$(kubectl -n "$NAMESPACE" get objectstore "$BARMAN_OBJECT_NAME" -o jsonpath='{.spec.retentionPolicy}' 2>/dev/null || true)
access_key_secret=$(kubectl -n "$NAMESPACE" get objectstore "$BARMAN_OBJECT_NAME" -o jsonpath='{.spec.configuration.s3Credentials.accessKeyId.name}' 2>/dev/null || true)
access_key_name=$(kubectl -n "$NAMESPACE" get objectstore "$BARMAN_OBJECT_NAME" -o jsonpath='{.spec.configuration.s3Credentials.accessKeyId.key}' 2>/dev/null || true)
secret_key_secret=$(kubectl -n "$NAMESPACE" get objectstore "$BARMAN_OBJECT_NAME" -o jsonpath='{.spec.configuration.s3Credentials.secretAccessKey.name}' 2>/dev/null || true)
secret_key_name=$(kubectl -n "$NAMESPACE" get objectstore "$BARMAN_OBJECT_NAME" -o jsonpath='{.spec.configuration.s3Credentials.secretAccessKey.key}' 2>/dev/null || true)
region_secret=$(kubectl -n "$NAMESPACE" get objectstore "$BARMAN_OBJECT_NAME" -o jsonpath='{.spec.configuration.s3Credentials.region.name}' 2>/dev/null || true)
region_name=$(kubectl -n "$NAMESPACE" get objectstore "$BARMAN_OBJECT_NAME" -o jsonpath='{.spec.configuration.s3Credentials.region.key}' 2>/dev/null || true)
[[ "$destination" == "s3://$GARAGE_BACKUP_BUCKET/" ]] || return 1
[[ "$endpoint" == "$GARAGE_S3_ENDPOINT" ]] || return 1
[[ "$retention" == "$RETENTION_POLICY" ]] || return 1
[[ "$access_key_secret" == "$GARAGE_BACKUP_SECRET_NAME" ]] || return 1
[[ "$access_key_name" == "ACCESS_KEY_ID" ]] || return 1
[[ "$secret_key_secret" == "$GARAGE_BACKUP_SECRET_NAME" ]] || return 1
[[ "$secret_key_name" == "SECRET_ACCESS_KEY" ]] || return 1
[[ "$region_secret" == "$GARAGE_BACKUP_SECRET_NAME" ]] || return 1
[[ "$region_name" == "REGION" ]] || return 1
return 0
}
ensure_barman_object_store_config() {
local attempts=${OBJECTSTORE_RECONCILE_ATTEMPTS:-3}
local i
for (( i=1; i<=attempts; i++ )); do
apply_barman_object_store
if objectstore_spec_matches_expected; then
return 0
fi
echo "WARN: ObjectStore '$BARMAN_OBJECT_NAME' spec does not match expected Garage configuration (attempt ${i}/${attempts}); reapplying ..."
sleep 2
done
echo "ERROR: ObjectStore '$BARMAN_OBJECT_NAME' does not match expected Garage configuration after ${attempts} attempts." >&2
kubectl -n "$NAMESPACE" get objectstore "$BARMAN_OBJECT_NAME" -o yaml 2>/dev/null >&2 || true
return 1
}
ensure_barman_plugin_config() {
local plugin_names plugin_present
local plugin_names plugin_present barman_obj_name plugin_enabled plugin_wal_archiver
plugin_names=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.spec.plugins[*].name}' 2>/dev/null || true)
plugin_present=$(printf '%s\n' "$plugin_names" | tr ' ' '\n' | grep -F "$BARMAN_PLUGIN_NAME" || true)
if [[ -n "$plugin_present" ]]; then
return 0
# Plugin name present — verify it's fully active for WAL archiving and object store routing.
barman_obj_name=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" \
-o jsonpath="{range .spec.plugins[?(@.name==\"$BARMAN_PLUGIN_NAME\")]}{.parameters.barmanObjectName}{end}" 2>/dev/null || true)
plugin_enabled=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" \
-o jsonpath="{range .spec.plugins[?(@.name==\"$BARMAN_PLUGIN_NAME\")]}{.enabled}{end}" 2>/dev/null || true)
plugin_wal_archiver=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" \
-o jsonpath="{range .spec.plugins[?(@.name==\"$BARMAN_PLUGIN_NAME\")]}{.isWALArchiver}{end}" 2>/dev/null || true)
if [[ "$barman_obj_name" == "$BARMAN_OBJECT_NAME" && "${plugin_enabled,,}" == "true" && "${plugin_wal_archiver,,}" == "true" ]]; then
return 0
fi
echo "Updating Barman plugin entry: ensuring enabled=true, isWALArchiver=true, and barmanObjectName=$BARMAN_OBJECT_NAME ..."
local idx
idx=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" \
-o json 2>/dev/null \
| python3 -c "import sys,json; items=json.load(sys.stdin)['spec'].get('plugins',[]); print(next((i for i,p in enumerate(items) if p.get('name')=='barman-cloud.cloudnative-pg.io'),-1))")
if [[ "$idx" -ge 0 ]]; then
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type json -p "[
{
\"op\": \"replace\",
\"path\": \"/spec/plugins/$idx\",
\"value\": {
\"enabled\": true,
\"name\": \"$BARMAN_PLUGIN_NAME\",
\"isWALArchiver\": true,
\"parameters\": {\"barmanObjectName\": \"$BARMAN_OBJECT_NAME\"}
}
}
]"
return 0
fi
# Fallback: append a correctly configured entry
fi
local plugins_json
@ -405,10 +547,11 @@ SCHEDULEDBACKUP
wait_for_plugin_ready() {
local start_time now plugin_names deployment_rows ns ready replicas
local plugin_deploy_ready pod socket_path
local plugin_deploy_ready plugin_ns svc_ns svc_name client_secret server_secret plugin_port
start_time=$(date +%s)
while true; do
plugin_deploy_ready=0
plugin_ns=""
plugin_names=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.spec.plugins[*].name}' 2>/dev/null || true)
if printf '%s\n' "$plugin_names" | tr ' ' '\n' | grep -Fxq "$BARMAN_PLUGIN_NAME"; then
deployment_rows=$(kubectl get deployment -A -l app.kubernetes.io/name=barman-cloud -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.status.readyReplicas}{"\t"}{.status.replicas}{"\n"}{end}' 2>/dev/null || true)
@ -422,22 +565,37 @@ wait_for_plugin_ready() {
replicas=${replicas:-0}
if (( ready >= 1 && replicas >= 1 )); then
plugin_deploy_ready=1
plugin_ns="$ns"
break
fi
done <<< "$deployment_rows"
if (( plugin_deploy_ready == 1 )); then
pod=$(kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME,cnpg.io/instanceRole=primary" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -z "$pod" ]]; then
pod=$(kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
# The Barman Cloud plugin is deployed as a standalone CNPG-I plugin.
# In this model, there is no Unix socket expected inside the database pod.
# Instead, CNPG discovers the plugin via a Kubernetes Service annotated with
# cnpg.io/pluginClientSecret, cnpg.io/pluginServerSecret and cnpg.io/pluginPort.
#
# We validate that registration artifact exists and that the referenced TLS
# secrets are present. The subsequent backup operation will fail if the plugin
# is still not usable.
svc_ns="${plugin_ns:-$CNPG_OPERATOR_NAMESPACE}"
svc_name=$(kubectl -n "$svc_ns" get svc -l "cnpg.io/pluginName=$BARMAN_PLUGIN_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -z "$svc_name" ]]; then
# Fallback for older manifests that don't set the cnpg.io/pluginName label
svc_name=$(kubectl -n "$svc_ns" get svc barman-cloud -o jsonpath='{.metadata.name}' 2>/dev/null || true)
fi
socket_path="$PLUGIN_SOCKET_DIR/$BARMAN_PLUGIN_NAME"
if [[ -n "$pod" ]]; then
if kubectl -n "$NAMESPACE" exec "$pod" -c postgres -- test -S "$socket_path" >/dev/null 2>&1; then
return 0
fi
if kubectl -n "$NAMESPACE" exec "$pod" -c manager -- test -S "$socket_path" >/dev/null 2>&1; then
return 0
if [[ -n "$svc_name" ]]; then
client_secret=$(kubectl -n "$svc_ns" get svc "$svc_name" -o jsonpath='{.metadata.annotations.cnpg\.io/pluginClientSecret}' 2>/dev/null || true)
server_secret=$(kubectl -n "$svc_ns" get svc "$svc_name" -o jsonpath='{.metadata.annotations.cnpg\.io/pluginServerSecret}' 2>/dev/null || true)
plugin_port=$(kubectl -n "$svc_ns" get svc "$svc_name" -o jsonpath='{.metadata.annotations.cnpg\.io/pluginPort}' 2>/dev/null || true)
if [[ -n "$client_secret" && -n "$server_secret" && -n "$plugin_port" ]]; then
if kubectl -n "$svc_ns" get secret "$client_secret" >/dev/null 2>&1 && kubectl -n "$svc_ns" get secret "$server_secret" >/dev/null 2>&1; then
return 0
fi
fi
fi
fi
@ -454,6 +612,165 @@ wait_for_plugin_ready() {
done
}
wait_for_barman_plugin_infra_ready() {
# Ensure the plugin controller and its registration artifacts (Service annotations + TLS secrets)
# are present *before* patching the CNPG Cluster to depend on the plugin.
local start_time now
local deployment_rows ns ready replicas
local plugin_deploy_ready plugin_ns svc_ns svc_name client_secret server_secret plugin_port
start_time=$(date +%s)
while true; do
plugin_deploy_ready=0
plugin_ns=""
deployment_rows=$(kubectl get deployment -A -l app.kubernetes.io/name=barman-cloud -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.status.readyReplicas}{"\t"}{.status.replicas}{"\n"}{end}' 2>/dev/null || true)
if [[ -z "$deployment_rows" ]]; then
deployment_rows=$(kubectl get deployment -A -o jsonpath='{range .items[?(@.metadata.name=="barman-cloud")]}{.metadata.namespace}{"\t"}{.status.readyReplicas}{"\t"}{.status.replicas}{"\n"}{end}' 2>/dev/null || true)
fi
while IFS=$'\t' read -r ns ready replicas; do
[[ -z "$ns" ]] && continue
ready=${ready:-0}
replicas=${replicas:-0}
if (( ready >= 1 && replicas >= 1 )); then
plugin_deploy_ready=1
plugin_ns="$ns"
break
fi
done <<< "$deployment_rows"
if (( plugin_deploy_ready == 1 )); then
svc_ns="${plugin_ns:-$CNPG_OPERATOR_NAMESPACE}"
svc_name=$(kubectl -n "$svc_ns" get svc -l "cnpg.io/pluginName=$BARMAN_PLUGIN_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -z "$svc_name" ]]; then
svc_name=$(kubectl -n "$svc_ns" get svc barman-cloud -o jsonpath='{.metadata.name}' 2>/dev/null || true)
fi
if [[ -n "$svc_name" ]]; then
client_secret=$(kubectl -n "$svc_ns" get svc "$svc_name" -o jsonpath='{.metadata.annotations.cnpg\.io/pluginClientSecret}' 2>/dev/null || true)
server_secret=$(kubectl -n "$svc_ns" get svc "$svc_name" -o jsonpath='{.metadata.annotations.cnpg\.io/pluginServerSecret}' 2>/dev/null || true)
plugin_port=$(kubectl -n "$svc_ns" get svc "$svc_name" -o jsonpath='{.metadata.annotations.cnpg\.io/pluginPort}' 2>/dev/null || true)
if [[ -n "$client_secret" && -n "$server_secret" && -n "$plugin_port" ]]; then
if kubectl -n "$svc_ns" get secret "$client_secret" >/dev/null 2>&1 && kubectl -n "$svc_ns" get secret "$server_secret" >/dev/null 2>&1; then
return 0
fi
fi
fi
fi
now=$(date +%s)
if (( now - start_time >= PLUGIN_READY_TIMEOUT )); then
echo "ERROR: Timed out waiting for Barman plugin infrastructure to become ready." >&2
return 1
fi
echo "Waiting for Barman plugin infrastructure (deployment + service registration) ..."
sleep 5
done
}
continuous_archiving_condition_line() {
kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" \
-o jsonpath='{range .status.conditions[?(@.type=="ContinuousArchiving")]}{.status}{"\t"}{.reason}{"\t"}{.message}{"\n"}{end}' 2>/dev/null || true
}
wait_for_continuous_archiving_ready() {
local timeout=${1:-$PLUGIN_READY_TIMEOUT}
local start_time now elapsed
local reconcile_count=0
local reconcile_limit=${PLUGIN_BACKUP_RETRY_LIMIT:-6}
start_time=$(date +%s)
while true; do
local ca_line ca_status ca_reason ca_message ca_status_lc ca_reason_lc ca_message_lc
ca_line=$(continuous_archiving_condition_line)
ca_status=""
ca_reason=""
ca_message=""
if [[ -n "$ca_line" ]]; then
IFS=$'\t' read -r ca_status ca_reason ca_message <<< "$ca_line"
fi
ca_status_lc=$(echo "${ca_status:-}" | tr '[:upper:]' '[:lower:]')
ca_reason_lc=$(echo "${ca_reason:-}" | tr '[:upper:]' '[:lower:]')
ca_message_lc=$(echo "${ca_message:-}" | tr '[:upper:]' '[:lower:]')
if [[ "$ca_status_lc" == "true" ]]; then
return 0
fi
if [[ "$ca_reason_lc" == *"continuousarchivingfailing"* || "$ca_message_lc" == *"wal archive plugin is not available"* || "$ca_message_lc" == *"plugin is not available"* ]]; then
if (( reconcile_count < reconcile_limit )); then
reconcile_count=$((reconcile_count + 1))
echo "WARN: ContinuousArchiving reports plugin unavailable. Reconciling plugin config (${reconcile_count}/${reconcile_limit}) ..."
ensure_barman_plugin_config || true
wait_for_plugin_ready || true
fi
fi
now=$(date +%s)
elapsed=$((now - start_time))
if (( elapsed >= timeout )); then
echo "ERROR: Timed out waiting for ContinuousArchiving to become ready (status=${ca_status:-<unset>} reason=${ca_reason:-<unset>} message=${ca_message:-<unset>})." >&2
return 1
fi
sleep 5
done
}
wait_for_objectstore_ready() {
local timeout=${1:-$OBJECTSTORE_READY_TIMEOUT}
local start_time now elapsed
start_time=$(date +%s)
echo "Waiting for ObjectStore '$BARMAN_OBJECT_NAME' to be Ready (timeout: ${timeout}s)..."
while true; do
local ready_cond phase
ready_cond=$(kubectl -n "$NAMESPACE" get objectstore "$BARMAN_OBJECT_NAME" \
-o jsonpath='{range .status.conditions[?(@.type=="Ready")]}{.status}{end}' 2>/dev/null || true)
phase=$(kubectl -n "$NAMESPACE" get objectstore "$BARMAN_OBJECT_NAME" \
-o jsonpath='{.status.phase}' 2>/dev/null || true)
# barman-cloud v0.11+ signals readiness via serverRecoveryWindow rather than
# a Ready condition; accept either form.
local recovery_window
recovery_window=$(kubectl -n "$NAMESPACE" get objectstore "$BARMAN_OBJECT_NAME" \
-o jsonpath='{.status.serverRecoveryWindow}' 2>/dev/null || true)
if [[ "$ready_cond" == "True" || "$ready_cond" == "true" || "$phase" == "Ready" || "$phase" == "ready" || -n "$recovery_window" ]]; then
echo "ObjectStore '$BARMAN_OBJECT_NAME' is Ready."
return 0
fi
now=$(date +%s)
elapsed=$((now - start_time))
# Some plugin/controller versions do not currently set Ready/phase status on
# ObjectStore. In that case, continue only when the ObjectStore exists,
# barman-cloud controller is available, and the ObjectStore spec still matches
# the expected Garage-backed configuration.
local object_exists barman_available generation
object_exists=$(kubectl -n "$NAMESPACE" get objectstore "$BARMAN_OBJECT_NAME" -o name 2>/dev/null || true)
barman_available=$(kubectl -n "$CNPG_OPERATOR_NAMESPACE" get deploy barman-cloud -o jsonpath='{.status.availableReplicas}' 2>/dev/null || true)
generation=$(kubectl -n "$NAMESPACE" get objectstore "$BARMAN_OBJECT_NAME" -o jsonpath='{.metadata.generation}' 2>/dev/null || true)
if [[ -n "$object_exists" && "$barman_available" =~ ^[1-9][0-9]*$ && "$generation" =~ ^[0-9]+$ && $elapsed -ge 30 ]] && objectstore_spec_matches_expected; then
echo "WARN: ObjectStore status fields are not populated, but barman-cloud is available and ObjectStore spec matches expected Garage configuration; continuing."
return 0
fi
if (( elapsed >= timeout )); then
echo "ERROR: Timed out waiting for ObjectStore '$BARMAN_OBJECT_NAME' to become Ready." >&2
kubectl -n "$NAMESPACE" get objectstore "$BARMAN_OBJECT_NAME" -o yaml 2>/dev/null >&2 || true
return 1
fi
sleep "$OBJECTSTORE_READY_INTERVAL"
done
}
has_successful_base_backup() {
local rows name phase method backup_type phase_lc method_lc backup_type_lc
rows=$(kubectl get backup -n "$NAMESPACE" -o jsonpath="{range .items[?(@.spec.cluster.name=='$CNPG_CLUSTER_NAME')]}{.metadata.name}{\"\t\"}{.status.phase}{\"\t\"}{.spec.method}{\"\t\"}{.spec.pluginConfiguration.parameters.backupType}{\"\n\"}{end}" 2>/dev/null || true)
@ -484,6 +801,9 @@ has_successful_base_backup() {
wait_for_successful_base_backup() {
local start_time now elapsed
local plugin_retry_count=0
local plugin_retry_limit=${PLUGIN_BACKUP_RETRY_LIMIT:-6}
local plugin_retry_interval=${PLUGIN_BACKUP_RETRY_INTERVAL:-20}
start_time=$(date +%s)
while true; do
if [[ -n "$LAST_BACKUP_NAME" ]]; then
@ -495,12 +815,55 @@ wait_for_successful_base_backup() {
return 0
;;
Failed|failed)
local err_lc
err_lc=$(echo "${err:-}" | tr '[:upper:]' '[:lower:]')
if [[ "$err_lc" == *"requested plugin is not available"* || "$err_lc" == *"wal archive plugin is not available"* || "$err_lc" == *"plugin is not available"* ]] && (( plugin_retry_count < plugin_retry_limit )); then
plugin_retry_count=$((plugin_retry_count + 1))
echo "WARN: Backup '$LAST_BACKUP_NAME' failed because plugin is not yet available. Retry ${plugin_retry_count}/${plugin_retry_limit} ..."
wait_for_plugin_ready || true
local retry_sleep=$((plugin_retry_interval * plugin_retry_count))
if (( retry_sleep > 120 )); then
retry_sleep=120
fi
if (( retry_sleep > 0 )); then
echo "Waiting ${retry_sleep}s before retrying backup trigger ..."
sleep "$retry_sleep"
fi
LAST_BACKUP_NAME=""
trigger_backup
sleep 5
continue
fi
echo "ERROR: Backup '$LAST_BACKUP_NAME' failed: ${err:-<no error provided>}" >&2
return 1
;;
esac
fi
local ca_line ca_status ca_reason ca_message ca_status_lc ca_reason_lc ca_message_lc
ca_line=$(continuous_archiving_condition_line)
ca_status=""
ca_reason=""
ca_message=""
if [[ -n "$ca_line" ]]; then
IFS=$'\t' read -r ca_status ca_reason ca_message <<< "$ca_line"
fi
ca_status_lc=$(echo "${ca_status:-}" | tr '[:upper:]' '[:lower:]')
ca_reason_lc=$(echo "${ca_reason:-}" | tr '[:upper:]' '[:lower:]')
ca_message_lc=$(echo "${ca_message:-}" | tr '[:upper:]' '[:lower:]')
if [[ "$ca_status_lc" != "true" && ( "$ca_reason_lc" == *"continuousarchivingfailing"* || "$ca_message_lc" == *"wal archive plugin is not available"* || "$ca_message_lc" == *"plugin is not available"* ) ]] && (( plugin_retry_count < plugin_retry_limit )); then
plugin_retry_count=$((plugin_retry_count + 1))
echo "WARN: ContinuousArchiving is failing due to plugin availability. Retry ${plugin_retry_count}/${plugin_retry_limit} ..."
ensure_barman_plugin_config || true
wait_for_plugin_ready || true
wait_for_continuous_archiving_ready "$PLUGIN_READY_TIMEOUT" || true
LAST_BACKUP_NAME=""
trigger_backup
sleep 5
continue
fi
if has_successful_base_backup; then
return 0
fi
@ -558,6 +921,7 @@ BACKUP
status() {
ensure_tools
select_kube_context
echo "CNPG backup status for cluster '$CNPG_CLUSTER_NAME' (namespace: '$NAMESPACE')"
local backup_spec
@ -573,7 +937,7 @@ status() {
fi
local ca_line
ca_line=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{range .status.conditions[?(@.type=="ContinuousArchiving")]}{.status}{"\t"}{.reason}{"\t"}{.message}{"\n"}{end}' 2>/dev/null || true)
ca_line=$(continuous_archiving_condition_line)
if [[ -n "$ca_line" ]]; then
echo "ContinuousArchiving condition:"
printf '%s' "$ca_line" | awk -F$'\t' '{print "- status=" $1 (length($2)?" reason=" $2:"") (length($3)?" message=" $3:"")}'
@ -599,14 +963,29 @@ status() {
case "$ACTION" in
start)
ensure_tools
select_kube_context
ensure_namespace
wait_for_apiserver_ready 180
ensure_cluster
# Fast-path: skip re-initialization when continuous archiving is already healthy
# and at least one successful base backup exists. All the wait_for_* loops below
# are idempotent but slow — no need to re-run them on every installer pass.
_ca_status_fp=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" \
-o jsonpath='{range .status.conditions[?(@.type=="ContinuousArchiving")]}{.status}{end}' \
2>/dev/null || true)
if [[ "$_ca_status_fp" == "True" ]] && has_successful_base_backup; then
echo "[CNPG BACKUP] Continuous archiving healthy and base backup exists — skipping re-initialization."
exit 0
fi
resolve_garage_namespace_and_endpoint
ensure_garage_bucket_and_key
apply_barman_object_store
ensure_barman_plugin_config
wait_for_barman_plugin_infra_ready
ensure_barman_object_store_config
wait_for_objectstore_ready
ensure_cluster_backup_config
ensure_barman_plugin_config
wait_for_plugin_ready
wait_for_continuous_archiving_ready
ensure_scheduled_backup
if [[ "$RUN_FIRST_BACKUP" == "1" ]]; then
wait_for_cnpg_webhook 300
@ -623,17 +1002,23 @@ case "$ACTION" in
;;
backup)
ensure_tools
select_kube_context
ensure_namespace
wait_for_apiserver_ready 180
ensure_cluster
apply_barman_object_store
ensure_barman_plugin_config
resolve_garage_namespace_and_endpoint
wait_for_barman_plugin_infra_ready
ensure_barman_object_store_config
wait_for_objectstore_ready
ensure_cluster_backup_config
ensure_barman_plugin_config
wait_for_plugin_ready
wait_for_continuous_archiving_ready
wait_for_cnpg_webhook 300
trigger_backup "${2:-full}"
;;
status)
select_kube_context
wait_for_apiserver_ready 60 || true
status
;;

521
mock_val/init_cnpg_gke.sh Executable file
View File

@ -0,0 +1,521 @@
#!/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, DB_PASSWORD, 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:-knoe-db-0}"
CNPG_CLUSTER_NAME="${CNPG_CLUSTER_NAME:-knoe-db}"
CNPG_BACKUP_SA="${CNPG_BACKUP_SA:-cnpg-backup}"
ARTIFACT_REGISTRY="${ARTIFACT_REGISTRY:-}"
KNOE_DB_IMAGE_TAG="${KNOE_DB_IMAGE_TAG:-}"
DB_PASSWORD="${DB_PASSWORD:-}"
CNPG_DB_OWNER="${CNPG_DB_OWNER:-knoe}"
CNPG_STORAGE_CLASS_ORDER="${CNPG_STORAGE_CLASS_ORDER:-premium-rwo,ssd,premium,standard-rwo,garage-hdd,standard,dynamic-rwo}"
CNPG_STORAGE_WAIT_TIMEOUT="${CNPG_STORAGE_WAIT_TIMEOUT:-240}"
# v1.29.0+ is required: spec.serviceAccountName lets the cluster pods run as
# cnpg-backup-sa (annotated with iam.gke.io/gcp-service-account for WI), which
# is how the GCS-backed barman ObjectStore authenticates without a static key.
# Older operators (we used 1.24.0 historically) silently drop the field.
CNPG_OPERATOR_VERSION="${CNPG_OPERATOR_VERSION:-1.29.0}"
# ── 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] $*"; }
resolve_knoe_db_image_tag() {
[[ -n "$KNOE_DB_IMAGE_TAG" ]] && return
local mode_key="k8s"
local knoe_home
knoe_home="${KNOE_HOME:-$REPO_ROOT}"
local pg_version_file="$knoe_home/modes/$mode_key/conf/postgresql/.version"
local release_file="$knoe_home/modes/$mode_key/knoe-db/.version"
[[ -f "$pg_version_file" ]] || pg_version_file="$knoe_home/conf/postgresql/.version"
[[ -f "$release_file" ]] || release_file="$knoe_home/knoe-db/.version"
local pg_version release
if [[ -f "$pg_version_file" ]]; then
pg_version="$(tr -d '[:space:]' < "$pg_version_file")"
else
pg_version="17.7"
fi
if [[ -f "$release_file" ]]; then
release="$(tr -d '[:space:]' < "$release_file")"
else
release="43"
fi
if [[ "$release" =~ ^[0-9]+$ ]]; then
release="$(printf '%03d' "$release")"
fi
KNOE_DB_IMAGE_TAG="${pg_version}-${release}"
}
storage_class_available() {
local candidate="$1"
[[ -n "$candidate" ]] || return 1
kubectl get storageclass "$candidate" >/dev/null 2>&1
}
append_storage_candidate() {
local candidate="$1"
[[ -n "$candidate" ]] || return
case ",${CNPG_STORAGE_CLASS_CANDIDATES_RESOLVED}," in
*",${candidate},"*) return ;;
esac
if [[ -n "$CNPG_STORAGE_CLASS_CANDIDATES_RESOLVED" ]]; then
CNPG_STORAGE_CLASS_CANDIDATES_RESOLVED+="${IFS_COMMA}${candidate}"
else
CNPG_STORAGE_CLASS_CANDIDATES_RESOLVED="$candidate"
fi
}
storage_class_is_compatible() {
local candidate="$1"
case "$candidate" in
*rwx*|*RWX*|gcsfuse*|parallelstore-*|enterprise-rwx|enterprise-multishare-rwx|regional-rwx|premium-rwx|standard-rwx|synology-*|*iscsi*)
return 1
;;
*)
return 0
;;
esac
}
append_by_name_pattern() {
local pattern="$1"
local sc
while IFS= read -r sc; do
[[ -n "$sc" ]] || continue
storage_class_is_compatible "$sc" || continue
case "$sc" in
*"$pattern"*) append_storage_candidate "$sc" ;;
esac
done <<< "$AVAILABLE_STORAGE_CLASSES"
}
resolve_storage_class_candidates() {
AVAILABLE_STORAGE_CLASSES="$(kubectl get storageclass -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true)"
[[ -n "$AVAILABLE_STORAGE_CLASSES" ]] || {
echo "ERROR: unable to enumerate Kubernetes storage classes." >&2
exit 1
}
CNPG_STORAGE_CLASS_CANDIDATES_RESOLVED=""
IFS=',' read -r -a requested_classes <<< "$CNPG_STORAGE_CLASS_ORDER"
local requested
for requested in "${requested_classes[@]}"; do
requested="${requested//[[:space:]]/}"
[[ -n "$requested" ]] || continue
case "$requested" in
ssd)
append_by_name_pattern "ssd"
;;
premium)
append_by_name_pattern "premium"
;;
*)
if storage_class_available "$requested" && storage_class_is_compatible "$requested"; then
append_storage_candidate "$requested"
fi
;;
esac
done
local sc
while IFS= read -r sc; do
[[ -n "$sc" ]] || continue
storage_class_is_compatible "$sc" || continue
append_storage_candidate "$sc"
done <<< "$AVAILABLE_STORAGE_CLASSES"
[[ -n "$CNPG_STORAGE_CLASS_CANDIDATES_RESOLVED" ]] || {
echo "ERROR: no compatible storage classes available for CNPG provisioning." >&2
exit 1
}
}
render_cnpg_manifest() {
local storage_class="$1"
local rendered_manifest="$2"
ARTIFACT_REGISTRY="${ARTIFACT_REGISTRY}" KNOE_DB_IMAGE_TAG="${KNOE_DB_IMAGE_TAG}" \
envsubst '${ARTIFACT_REGISTRY} ${KNOE_DB_IMAGE_TAG}' < "$GKE_MANIFEST_DIR/knoe-db.yaml" > "$rendered_manifest"
sed -E "s#^([[:space:]]*storageClassName:).*#\\1 ${storage_class}#" "$rendered_manifest" > "${rendered_manifest}.tmp"
mv "${rendered_manifest}.tmp" "$rendered_manifest"
}
cnpg_attempt_ready() {
local expected_storage_class="$1"
local pfx current_storage_class
pfx="${CNPG_CLUSTER_NAME}-"
current_storage_class="$(kubectl -n "$CNPG_NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.spec.storage.pvcTemplate.storageClassName}' 2>/dev/null || true)"
[[ "$current_storage_class" == "$expected_storage_class" ]] || return 1
kubectl -n "$CNPG_NAMESPACE" get pvc -o jsonpath='{range .items[*]}{.metadata.name}{"|"}{.spec.storageClassName}{"|"}{.status.phase}{"\n"}{end}' 2>/dev/null \
| awk -F'|' -v expected="$expected_storage_class" -v prefix="$pfx" '$1 ~ ("^" prefix) && $2 == expected && $3 == "Bound" { found=1 } END { exit(found ? 0 : 1) }'
}
cnpg_attempt_has_pvc_for_class() {
local expected_storage_class="$1"
local pfx
pfx="${CNPG_CLUSTER_NAME}-"
kubectl -n "$CNPG_NAMESPACE" get pvc -o jsonpath='{range .items[*]}{.metadata.name}{"|"}{.spec.storageClassName}{"\n"}{end}' 2>/dev/null \
| awk -F'|' -v expected="$expected_storage_class" -v prefix="$pfx" '$1 ~ ("^" prefix) && $2 == expected { found=1 } END { exit(found ? 0 : 1) }'
}
cnpg_recent_events() {
kubectl -n "$CNPG_NAMESPACE" get events --sort-by=.lastTimestamp 2>/dev/null | tail -n 80 || true
}
wait_for_storage_outcome() {
local expected_storage_class="$1"
local started_at current_ts events
started_at="$(date +%s)"
while true; do
if cnpg_attempt_ready "$expected_storage_class"; then
return 0
fi
if cnpg_attempt_has_pvc_for_class "$expected_storage_class"; then
events="$(cnpg_recent_events)"
if printf '%s\n' "$events" | grep -q "SSD_TOTAL_GB"; then
return 10
fi
if printf '%s\n' "$events" | grep -Eqi "storageclass.*not found|failed to provision volume"; then
return 11
fi
fi
current_ts="$(date +%s)"
if (( current_ts - started_at >= CNPG_STORAGE_WAIT_TIMEOUT )); then
return 1
fi
sleep 10
done
}
reset_cnpg_cluster_attempt() {
kubectl -n "$CNPG_NAMESPACE" delete cluster "$CNPG_CLUSTER_NAME" --ignore-not-found=true --wait=true --timeout=180s >/dev/null 2>&1 || true
kubectl -n "$CNPG_NAMESPACE" delete job -l cnpg.io/cluster="$CNPG_CLUSTER_NAME" --ignore-not-found=true >/dev/null 2>&1 || true
kubectl -n "$CNPG_NAMESPACE" delete pvc -l cnpg.io/cluster="$CNPG_CLUSTER_NAME" --ignore-not-found=true >/dev/null 2>&1 || true
kubectl -n "$CNPG_NAMESPACE" delete pod -l cnpg.io/cluster="$CNPG_CLUSTER_NAME" --ignore-not-found=true >/dev/null 2>&1 || true
}
ensure_cnpg_tls_secrets() {
local cluster_name="knoe-db"
if kubectl -n "$CNPG_NAMESPACE" get secret "${cluster_name}-ca" >/dev/null 2>&1 \
&& kubectl -n "$CNPG_NAMESPACE" get secret "${cluster_name}-tls" >/dev/null 2>&1; then
local ca_key_b64
local ca_key_pem
ca_key_b64="$(kubectl -n "$CNPG_NAMESPACE" get secret "${cluster_name}-ca" -o jsonpath='{.data.ca\.key}' 2>/dev/null || true)"
ca_key_pem="$(printf '%s' "$ca_key_b64" | openssl base64 -d -A 2>/dev/null || true)"
if [[ -n "$ca_key_b64" && "$ca_key_pem" == *"BEGIN EC PRIVATE KEY"* ]]; then
log "CNPG TLS secrets already present in ${CNPG_NAMESPACE}."
return
fi
log "CNPG CA secret is missing/invalid for CNPG in ${CNPG_NAMESPACE}; regenerating CNPG TLS secrets ..."
fi
log "Bootstrapping CNPG TLS secrets in ${CNPG_NAMESPACE} ..."
local tmp_dir
tmp_dir="$(mktemp -d)"
local fqdn cn
fqdn="${cluster_name}-rw.${CNPG_NAMESPACE}.svc.cluster.local"
cn="$fqdn"
if [[ ${#cn} -gt 64 ]]; then
cn="${cluster_name}-rw"
fi
openssl ecparam -name prime256v1 -genkey -noout -out "$tmp_dir/ca.key" >/dev/null 2>&1
openssl req -x509 -new -nodes -key "$tmp_dir/ca.key" -sha256 -days 3650 \
-subj "/CN=${cluster_name}-ca" -out "$tmp_dir/ca.crt" >/dev/null 2>&1
openssl ecparam -name prime256v1 -genkey -noout -out "$tmp_dir/tls.key" >/dev/null 2>&1
openssl req -new -key "$tmp_dir/tls.key" -subj "/CN=${cn}" -out "$tmp_dir/tls.csr" >/dev/null 2>&1
cat > "$tmp_dir/ext.cnf" <<EOF
subjectAltName=DNS:${fqdn},DNS:${cluster_name},DNS:localhost,IP:127.0.0.1
extendedKeyUsage=serverAuth
EOF
openssl x509 -req -in "$tmp_dir/tls.csr" -CA "$tmp_dir/ca.crt" -CAkey "$tmp_dir/ca.key" \
-CAcreateserial -out "$tmp_dir/tls.crt" -days 3650 -sha256 -extfile "$tmp_dir/ext.cnf" >/dev/null 2>&1
kubectl -n "$CNPG_NAMESPACE" create secret generic "${cluster_name}-ca" \
--from-file=ca.crt="$tmp_dir/ca.crt" \
--from-file=ca.key="$tmp_dir/ca.key" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl -n "$CNPG_NAMESPACE" create secret tls "${cluster_name}-tls" \
--cert="$tmp_dir/tls.crt" \
--key="$tmp_dir/tls.key" \
--dry-run=client -o yaml | kubectl apply -f -
rm -rf "$tmp_dir"
}
ensure_db_user_secret() {
if kubectl -n "$CNPG_NAMESPACE" get secret knoe-db-user >/dev/null 2>&1; then
log "knoe-db-user secret already present in ${CNPG_NAMESPACE}."
else
[[ -n "$DB_PASSWORD" && "$DB_PASSWORD" != \$\{* ]] || {
echo "ERROR: secret 'knoe-db-user' missing in '${CNPG_NAMESPACE}' and DB_PASSWORD is not set." >&2
exit 1
}
log "Creating missing knoe-db-user secret in ${CNPG_NAMESPACE} ..."
kubectl -n "$CNPG_NAMESPACE" create secret generic knoe-db-user \
--from-literal=username="$CNPG_DB_OWNER" \
--from-literal=password="$DB_PASSWORD" \
--dry-run=client -o yaml | kubectl apply -f -
fi
if kubectl -n "$CNPG_NAMESPACE" get secret knoe-db-superuser >/dev/null 2>&1; then
return
fi
[[ -n "$DB_PASSWORD" && "$DB_PASSWORD" != \$\{* ]] || return
log "Creating missing knoe-db-superuser secret in ${CNPG_NAMESPACE} ..."
kubectl -n "$CNPG_NAMESPACE" create secret generic knoe-db-superuser \
--from-literal=username=postgres \
--from-literal=password="$DB_PASSWORD" \
--dry-run=client -o yaml | kubectl apply -f -
}
# ── Tool checks ──────────────────────────────────────────────────────────────
for tool in gcloud kubectl gsutil openssl; 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
# IAM propagation is eventually consistent right after service-account creation.
# Wait briefly so downstream gsutil IAM grants don't fail with transient 400.
for i in $(seq 1 12); do
if gcloud iam service-accounts describe "$GCP_SA_EMAIL" --project "$GCP_PROJECT_ID" >/dev/null 2>&1; then
break
fi
log "Waiting for service account propagation ($i/12) ..."
sleep 5
done
# ── 4. Grant storage access ───────────────────────────────────────────────────
log "Granting objectAdmin on backup buckets to $GCP_SA_EMAIL ..."
for bucket in "$GCS_BACKUP_BUCKET" "$GCS_WAL_BUCKET"; do
ok=0
for i in $(seq 1 6); do
if gsutil iam ch \
"serviceAccount:${GCP_SA_EMAIL}:objectAdmin" \
"gs://$bucket"; then
ok=1
break
fi
log "WARN: IAM grant failed for gs://$bucket (attempt $i/6); retrying in 5s ..."
sleep 5
done
[[ $ok -eq 1 ]] || {
echo "ERROR: failed to grant objectAdmin on gs://$bucket to $GCP_SA_EMAIL" >&2
exit 1
}
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 v${CNPG_OPERATOR_VERSION} ..."
kubectl apply --server-side -f \
"https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/main/releases/cnpg-${CNPG_OPERATOR_VERSION}.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 '${GCP_PROJECT_ID}' < "$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. Bootstrap CNPG secrets ───────────────────────────────────────────────
ensure_cnpg_tls_secrets
ensure_db_user_secret
# ── 10. Apply CNPG cluster ───────────────────────────────────────────────────
IFS_COMMA=","
resolve_knoe_db_image_tag
log "Using knoe-db image tag: ${KNOE_DB_IMAGE_TAG}"
resolve_storage_class_candidates
IFS=',' read -r -a storage_classes <<< "$CNPG_STORAGE_CLASS_CANDIDATES_RESOLVED"
selected_storage_class=""
attempt_total="${#storage_classes[@]}"
attempt_index=0
if kubectl -n "$CNPG_NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" >/dev/null 2>&1; then
existing_phase="$(kubectl -n "$CNPG_NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.status.phase}' 2>/dev/null || true)"
if [[ "$existing_phase" != "Cluster in healthy state" ]]; then
log "Existing cluster phase ${existing_phase:-unknown} is not healthy; resetting stale resources before storage-class evaluation."
reset_cnpg_cluster_attempt
fi
fi
for storage_class in "${storage_classes[@]}"; do
storage_class="${storage_class//[[:space:]]/}"
[[ -n "$storage_class" ]] || continue
attempt_index=$((attempt_index + 1))
log "Applying knoe-db CNPG cluster with storageClass=${storage_class} (${attempt_index}/${attempt_total}) ..."
tmp_manifest="$(mktemp)"
render_cnpg_manifest "$storage_class" "$tmp_manifest"
kubectl apply -f "$tmp_manifest"
rm -f "$tmp_manifest"
if wait_for_storage_outcome "$storage_class"; then
selected_storage_class="$storage_class"
break
fi
outcome_rc=$?
case "$outcome_rc" in
10)
log "WARN: storageClass=${storage_class} failed due to SSD quota pressure; stepping down."
;;
11)
log "WARN: storageClass=${storage_class} failed provisioning; stepping down."
;;
*)
log "WARN: storageClass=${storage_class} did not become ready within ${CNPG_STORAGE_WAIT_TIMEOUT}s; stepping down."
;;
esac
reset_cnpg_cluster_attempt
done
[[ -n "$selected_storage_class" ]] || {
echo "ERROR: CNPG failed to provision across storage class candidates: ${CNPG_STORAGE_CLASS_CANDIDATES_RESOLVED}" >&2
exit 1
}
log "CNPG provisioning selected storageClass=${selected_storage_class}."
# ── 11. Ensure RoleBindings reference cnpg-backup-sa ─────────────────────────
# CNPG auto-creates knoe-db and knoe-db-barman-cloud RoleBindings at cluster
# creation. With spec.serviceAccountName set from the start the operator may
# use cnpg-backup-sa as the sole subject, but if it still uses the default SA
# name (cluster name) we patch both bindings to add cnpg-backup-sa — matching
# the live state after the 2026-04-29 stabilization session.
log "Ensuring CNPG-managed RoleBindings include cnpg-backup-sa ..."
for rb in "${CNPG_CLUSTER_NAME}" "${CNPG_CLUSTER_NAME}-barman-cloud"; do
for _ in 1 2 3 4 5; do
kubectl -n "${CNPG_NAMESPACE}" get rolebinding "${rb}" >/dev/null 2>&1 && break
sleep 2
done
if kubectl -n "${CNPG_NAMESPACE}" get rolebinding "${rb}" >/dev/null 2>&1; then
if ! kubectl -n "${CNPG_NAMESPACE}" get rolebinding "${rb}" \
-o jsonpath='{.subjects[*].name}' | grep -qw cnpg-backup-sa; then
patch_file="$(mktemp)"
cat > "${patch_file}" <<EOF
[{"op":"add","path":"/subjects/-","value":{"kind":"ServiceAccount","name":"cnpg-backup-sa","namespace":"${CNPG_NAMESPACE}"}}]
EOF
kubectl -n "${CNPG_NAMESPACE}" patch rolebinding "${rb}" \
--type='json' --patch-file="${patch_file}" || true
rm -f "${patch_file}"
log " Patched RoleBinding ${rb} to add cnpg-backup-sa."
else
log " RoleBinding ${rb} already includes cnpg-backup-sa."
fi
else
log " WARNING: RoleBinding ${rb} not found — skipping patch."
fi
done
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

@ -11,10 +11,13 @@ _has_config=0
for _arg in "$@"; do
[[ "$_arg" == "-c" || "$_arg" == "--config" || "$_arg" == -c=* || "$_arg" == --config=* ]] && _has_config=1
done
if [[ $_has_config -eq 0 && -f "$SCRIPT_DIR/../conf/knoe.cfg" ]]; then
set -- "-c" "$SCRIPT_DIR/../conf/knoe.cfg" "$@"
if [[ $_has_config -eq 0 ]]; then
_default_cfg="$(common_core_default_config_path "$SCRIPT_DIR" || true)"
if [[ -n "$_default_cfg" ]]; then
set -- "-c" "$_default_cfg" "$@"
fi
fi
unset _has_config _arg
unset _has_config _arg _default_cfg
common_core_preparse_config "$@"
@ -25,7 +28,8 @@ usage() {
cat <<EOF
Usage: init_common_services.sh [-n|--namespace NS] [-k|--kerberos] <update|start|status|verify>
Deploys common infrastructure services (Registry, OpenTofu, Garage, OpenBao, Kong, Cert-Manager)
Deploys common infrastructure services (Registry, Garage, OpenBao, Kong, Cert-Manager;
OpenTofu on non-k8s modes)
into the given Kubernetes namespace. Use -k to include the Kerberos/KDC service.
EOF
}
@ -83,7 +87,8 @@ if [ -z "$NS" ]; then
NS="${SERVICE_NAMESPACE:-${NAMESPACE:-}}"
fi
if [ -z "$NS" ]; then
NS="default"
echo "ERROR: No namespace specified. Set SERVICE_NAMESPACE in knoe.cfg or pass -n/--namespace." >&2
exit 1
fi
# Registry should live in the common-core/service namespace unless explicitly overridden.
@ -91,8 +96,8 @@ REGISTRY_NS="${REGISTRY_NAMESPACE:-${NS}}"
knoe_ensure_kubeconfig >/dev/null 2>&1 || true
echo "DEBUG: knoe_ensure_kubeconfig finished" >&2
knoe_ensure_kube_context || exit 1
echo "DEBUG: knoe_ensure_kube_context finished" >&2
ensure_kube_context || exit 1
echo "DEBUG: ensure_kube_context finished" >&2
# Check cluster reachability early to fail fast
if [[ "$ACTION" != "status" ]]; then
@ -501,12 +506,19 @@ OPENTOFU_NAME=${OPENTOFU_NAME:-opentofu}
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
REGISTRY_NAME=${REGISTRY_NAME:-registry}
GARAGE_NAME=${GARAGE_NAME:-garage}
REDIS_NAME=${REDIS_NAME:-redis}
KONG_NAME=${KONG_NAME:-knoe-svc-kong}
KONG_CONFIG_NAME=${KONG_CONFIG_NAME:-knoe-svc-kong-config}
# In k8s (GKE/prod) mode use the knoe-svc-kong naming
if [ "${KNOE_MODE:-}" = "k8s" ]; then
KONG_NAME=${KONG_NAME/knoe-svc-kong/knoe-svc-kong}
KONG_CONFIG_NAME=${KONG_CONFIG_NAME/knoe-svc-kong/knoe-svc-kong}
fi
export KONG_NAME KONG_CONFIG_NAME KNOE_MODE
OPENTOFU_CONFIGMAP=${OPENTOFU_CONFIGMAP:-opentofu-nginx}
OPENTOFU_SECRET=${OPENTOFU_SECRET:-opentofu-admin}
GARAGE_CONFIGMAP=${GARAGE_CONFIGMAP:-garage-config}
GARAGE_SECRET_NAME=${GARAGE_SECRET_NAME:-garage-secrets}
KONG_CONFIG_NAME=${KONG_CONFIG_NAME:-knoe-svc-kong-config}
find_namespaces() {
local kind="$1"
@ -532,17 +544,19 @@ collect_other_namespaces() {
migrate_common_services() {
local old_ns
for old_ns in $(collect_other_namespaces "$NS" "$OPENTOFU_NAME" deployment service); do
echo "Found OpenTofu in namespace '$old_ns'; removing before deploy to '$NS' ..."
if [ -x "$SCRIPT_DIR/init_opentofu.sh" ]; then
"$SCRIPT_DIR/init_opentofu.sh" -n "$old_ns" stop || true
else
kubectl delete -n "$old_ns" deploy "$OPENTOFU_NAME" --ignore-not-found >/dev/null 2>&1 || true
kubectl delete -n "$old_ns" svc "$OPENTOFU_NAME" --ignore-not-found >/dev/null 2>&1 || true
fi
kubectl delete -n "$old_ns" configmap "$OPENTOFU_CONFIGMAP" --ignore-not-found >/dev/null 2>&1 || true
kubectl delete -n "$old_ns" secret "$OPENTOFU_SECRET" --ignore-not-found >/dev/null 2>&1 || true
done
if [ "${KNOE_MODE:-}" != "k8s" ]; then
for old_ns in $(collect_other_namespaces "$NS" "$OPENTOFU_NAME" deployment service); do
echo "Found OpenTofu in namespace '$old_ns'; removing before deploy to '$NS' ..."
if [ -x "$SCRIPT_DIR/init_opentofu.sh" ]; then
"$SCRIPT_DIR/init_opentofu.sh" -n "$old_ns" stop || true
else
kubectl delete -n "$old_ns" deploy "$OPENTOFU_NAME" --ignore-not-found >/dev/null 2>&1 || true
kubectl delete -n "$old_ns" svc "$OPENTOFU_NAME" --ignore-not-found >/dev/null 2>&1 || true
fi
kubectl delete -n "$old_ns" configmap "$OPENTOFU_CONFIGMAP" --ignore-not-found >/dev/null 2>&1 || true
kubectl delete -n "$old_ns" secret "$OPENTOFU_SECRET" --ignore-not-found >/dev/null 2>&1 || true
done
fi
for old_ns in $(collect_other_namespaces "$REGISTRY_NS" "$REGISTRY_NAME" deployment service); do
echo "Found Registry ($REGISTRY_NAME) in namespace '$old_ns'; removing before deploy to '$REGISTRY_NS' ..."
@ -573,6 +587,16 @@ migrate_common_services() {
fi
done
for old_ns in $(collect_other_namespaces "$NS" "$REDIS_NAME" statefulset deployment service); do
echo "Found Redis in namespace '$old_ns'; removing before deploy to '$NS' ..."
if [ -x "$SCRIPT_DIR/init_redis.sh" ]; then
"$SCRIPT_DIR/init_redis.sh" -n "$old_ns" stop || true
else
kubectl delete -n "$old_ns" statefulset "${REDIS_NAME}-master" --ignore-not-found >/dev/null 2>&1 || true
kubectl delete -n "$old_ns" svc "${REDIS_NAME}-master" "${REDIS_NAME}-headless" --ignore-not-found >/dev/null 2>&1 || true
fi
done
# Kong API gateway (service layer)
local kong_old_namespaces=""
kong_old_namespaces+=$(collect_other_namespaces "$NS" "$KONG_NAME" deployment service)
@ -584,6 +608,53 @@ migrate_common_services() {
kubectl delete -n "$old_ns" svc "$KONG_NAME" --ignore-not-found >/dev/null 2>&1 || true
kubectl delete -n "$old_ns" configmap "$KONG_CONFIG_NAME" --ignore-not-found >/dev/null 2>&1 || true
done
# Same-namespace: remove the alternate Kong deployment name if present.
# In k8s/GKE mode KONG_NAME=knoe-svc-kong; a leftover knoe-svc-kong (or vice
# versa) in the same namespace causes duplicate pods that the deployer won't
# clean up on its own.
local kong_alt_name kong_alt_config
case "$KONG_NAME" in
*knoe-svc-kong*) kong_alt_name="${KONG_NAME//knoe-svc-kong/knoe-svc-kong}"
kong_alt_config="${KONG_CONFIG_NAME//knoe-svc-kong/knoe-svc-kong}" ;;
*knoe-svc-kong*) kong_alt_name="${KONG_NAME//knoe-svc-kong/knoe-svc-kong}"
kong_alt_config="${KONG_CONFIG_NAME//knoe-svc-kong/knoe-svc-kong}" ;;
*) kong_alt_name="" ; kong_alt_config="" ;;
esac
if [[ -n "$kong_alt_name" && "$kong_alt_name" != "$KONG_NAME" ]]; then
if kubectl -n "$NS" get deploy "$kong_alt_name" >/dev/null 2>&1; then
echo "Found stale alternate Kong deployment '$kong_alt_name' in '$NS'; removing ..."
kubectl -n "$NS" delete deploy "$kong_alt_name" --ignore-not-found >/dev/null 2>&1 || true
kubectl -n "$NS" delete svc "$kong_alt_name" --ignore-not-found >/dev/null 2>&1 || true
[[ -n "$kong_alt_config" ]] && \
kubectl -n "$NS" delete configmap "$kong_alt_config" --ignore-not-found >/dev/null 2>&1 || true
fi
fi
# Same-namespace: prune excess unhealthy Kong pods from a stuck rolling update.
# Happens when the old pod (e.g. OOMKilled) does not terminate cleanly before
# the new pod comes up, leaving the deployment with more pods than desired.
if kubectl -n "$NS" get deploy "$KONG_NAME" >/dev/null 2>&1; then
local _kong_desired _kong_pod_count _stale_pod
_kong_desired=$(kubectl -n "$NS" get deploy "$KONG_NAME" \
-o jsonpath='{.spec.replicas}' 2>/dev/null)
_kong_desired="${_kong_desired:-1}"
_kong_pod_count=$(kubectl -n "$NS" get pods \
-l "app=${KONG_NAME}" --no-headers 2>/dev/null | wc -l | tr -d '[:space:]')
if [[ "${_kong_pod_count:-0}" -gt "${_kong_desired:-1}" ]]; then
echo "Kong ($KONG_NAME) has ${_kong_pod_count} pod(s) but desired=${_kong_desired};" \
"removing unhealthy pods ..."
while IFS= read -r _stale_pod; do
[[ -z "$_stale_pod" ]] && continue
echo " Removing unhealthy pod: $_stale_pod"
kubectl -n "$NS" delete pod "$_stale_pod" --force --grace-period=0 \
>/dev/null 2>&1 || true
done < <(
kubectl -n "$NS" get pods -l "app=${KONG_NAME}" --no-headers 2>/dev/null \
| awk '{split($2,r,"/"); ok=($3=="Running" && r[1]==r[2] && r[1]~/^[0-9]+$/); if(!ok) print $1}'
)
fi
fi
}
echo "Deploying common services (namespace=$NS, action=$ACTION)"
@ -601,14 +672,19 @@ esac
# 1. Registry no dependencies; other services pull images from it
# 2. OpenBao secrets vault; needed by downstream services
# 3. Garage object storage
# 4. OpenTofu IaC engine; depends on registry + secrets
# 4. OpenTofu IaC engine; depends on registry + secrets (non-k8s)
# ---------------------------------------------------------------------------
if [ -x "$SCRIPT_DIR/init_registry.sh" ]; then
REGISTRY_NAMESPACE="$REGISTRY_NS" SERVICE_NAMESPACE="$NS" \
"$SCRIPT_DIR/init_registry.sh" -n "$REGISTRY_NS" "$ACTION" || rc=$?
if [ "${KNOE_MODE:-}" = "k8s" ]; then
echo "[INFO] Registry update namespace=${REGISTRY_NS}"
echo "[INFO] GKE/prod mode: skipping in-cluster Docker registry (using GCP Artifact Registry)."
else
echo "WARN: init_registry.sh not found; registry deploy skipped."
if [ -x "$SCRIPT_DIR/init_registry.sh" ]; then
REGISTRY_NAMESPACE="$REGISTRY_NS" SERVICE_NAMESPACE="$NS" \
"$SCRIPT_DIR/init_registry.sh" -n "$REGISTRY_NS" "$ACTION" || rc=$?
else
echo "WARN: init_registry.sh not found; registry deploy skipped."
fi
fi
if [ -x "$SCRIPT_DIR/init_openbao.sh" ]; then
@ -618,6 +694,13 @@ else
echo "WARN: init_openbao.sh not found; skipping OpenBao."
fi
if [ -x "$SCRIPT_DIR/init_redis.sh" ]; then
REDIS_NAMESPACE="$NS" SERVICE_NAMESPACE="$NS" \
"$SCRIPT_DIR/init_redis.sh" -n "$NS" "$ACTION" || rc=$?
else
echo "WARN: init_redis.sh not found; Redis deploy skipped."
fi
if [ -x "$SCRIPT_DIR/init_garage_store.sh" ]; then
garage_action="$ACTION"
case "$garage_action" in
@ -629,24 +712,32 @@ else
echo "WARN: init_garage_store.sh not found; garage deploy skipped."
fi
if [ -x "$SCRIPT_DIR/init_opentofu.sh" ]; then
if [ "${KNOE_MODE:-}" = "k8s" ]; then
echo "[INFO] k8s mode: skipping OpenTofu deploy."
elif [ -x "$SCRIPT_DIR/init_opentofu.sh" ]; then
ROLLOUT_TIMEOUT="${ROLLOUT_TIMEOUT:-300s}" "$SCRIPT_DIR/init_opentofu.sh" -n "$NS" "$ACTION" || rc=$?
else
echo "WARN: init_opentofu.sh not found; skipping OpenTofu."
fi
if [[ "$ENABLE_KERBEROS" == "1" ]]; then
if [ -x "$SCRIPT_DIR/init_kdc.sh" ]; then
kdc_action="$ACTION"
case "$kdc_action" in
stop) kdc_action="cleanup" ;;
status) kdc_action="status" ;;
*) kdc_action="update" ;;
esac
SERVICE_NAMESPACE="$NS" PROLE_KDC_NAMESPACE="$NS" \
"$SCRIPT_DIR/init_kdc.sh" "$kdc_action" || rc=$?
# KDC is now embedded in the `knoe-auth` pod (multi-container) by default.
# Only deploy a standalone KDC when explicitly requested.
if [[ "${PROLE_KDC_STANDALONE:-0}" == "1" ]]; then
if [ -x "$SCRIPT_DIR/init_kdc.sh" ]; then
kdc_action="$ACTION"
case "$kdc_action" in
stop) kdc_action="cleanup" ;;
status) kdc_action="status" ;;
*) kdc_action="update" ;;
esac
SERVICE_NAMESPACE="$NS" PROLE_KDC_NAMESPACE="$NS" \
"$SCRIPT_DIR/init_kdc.sh" "$kdc_action" || rc=$?
else
echo "WARN: init_kdc.sh not found; standalone KDC deploy skipped."
fi
else
echo "WARN: init_kdc.sh not found; kerberos deploy skipped."
echo "[INFO] Kerberos enabled: skipping standalone KDC deploy (KDC runs as sidecar in knoe-auth)."
fi
fi

View File

@ -5,7 +5,7 @@ set -euo pipefail
# - Build and deploy the knoe-db-manager Node.js REST endpoint
# - Provides /backup/<cluster>/full to trigger barman full backups into Garage
# - Deploys to the knoe-db namespace; accessible via Kong endpoint /backup
# - Deploys to the current cluster (k3d or k3s) based on KNOE_MODE from conf/knoe.cfg
# - Deploys to the current cluster (k3d or k3s) based on KNOE_MODE from active config
#
# Usage:
# ./init_db_manager.sh [--mode MODE] <start|stop|status|restart|build>

View File

@ -20,7 +20,7 @@ Usage:
Options:
--mode <k3d|k3s|k8s|local> Deployment mode (default: ${MODE:-k3d})
-n, --namespace <name> Target namespace (default: forgejo)
-c, --config <knoe.cfg> Path to knoe.cfg (defaults to detected)
-c, --config <config.cfg> Path to config file (defaults to detected)
--force Delete existing Forgejo resources before deploy
--help Show this help
@ -49,11 +49,12 @@ while [[ $# -gt 0 ]]; do
esac
done
# Resolve config path and namespace defaults from knoe.cfg when present
if [[ -z "$CFG_PATH" && -n "${KNOE_CONF:-}" && -f "${KNOE_CONF}/knoe.cfg" ]]; then
CFG_PATH="${KNOE_CONF}/knoe.cfg"
elif [[ -z "$CFG_PATH" && -f "$SCRIPT_DIR/../conf/knoe.cfg" ]]; then
CFG_PATH="$SCRIPT_DIR/../conf/knoe.cfg"
# Resolve config path and namespace defaults when present
if [[ -z "$CFG_PATH" && -n "${KNOE_CONF:-}" ]]; then
CFG_PATH="$(_knoe_cfg_select_cfg_file "${KNOE_CONF}")"
fi
if [[ -z "$CFG_PATH" ]]; then
CFG_PATH="$(_knoe_cfg_select_cfg_file "$SCRIPT_DIR/../conf")"
fi
if [[ -z "$NAMESPACE" && -n "$CFG_PATH" ]]; then

View File

@ -49,6 +49,47 @@ GARAGE_NODE_CAPACITY=${GARAGE_NODE_CAPACITY:-10GB}
GARAGE_ZONE=${GARAGE_ZONE:-local}
GARAGE_NAMESPACE=${GARAGE_NAMESPACE:-$RESOLVED_NAMESPACE}
NAMESPACE="$GARAGE_NAMESPACE"
APP_CLUSTER_KUBECONTEXT=${APP_CLUSTER_KUBECONTEXT:-${init_cluster_app_cluster_kubecontext:-}}
DB_CLUSTER_KUBECONTEXT=${DB_CLUSTER_KUBECONTEXT:-${init_cluster_db_cluster_kubecontext:-}}
GARAGE_AUTHORITY_ROLE=${GARAGE_AUTHORITY_ROLE:-db}
GARAGE_AUTHORITY_ROLE="${GARAGE_AUTHORITY_ROLE,,}"
ACTIVE_KUBE_CONTEXT=${KUBECTL_CONTEXT:-${KUBE_CONTEXT_NAME:-${KUBECONTEXT:-}}}
garage_context_role() {
local ctx="${ACTIVE_KUBE_CONTEXT:-}"
if [[ -z "$ctx" ]]; then
ctx=$(kubectl config current-context 2>/dev/null || true)
fi
if [[ -n "$APP_CLUSTER_KUBECONTEXT" && "$ctx" == "$APP_CLUSTER_KUBECONTEXT" ]]; then
printf '%s' "app"
return 0
fi
if [[ -n "$DB_CLUSTER_KUBECONTEXT" && "$ctx" == "$DB_CLUSTER_KUBECONTEXT" ]]; then
printf '%s' "db"
return 0
fi
printf '%s' "unknown"
}
garage_split_cluster_contexts_enabled() {
[[ -n "$APP_CLUSTER_KUBECONTEXT" && -n "$DB_CLUSTER_KUBECONTEXT" && "$APP_CLUSTER_KUBECONTEXT" != "$DB_CLUSTER_KUBECONTEXT" ]]
}
garage_is_authoritative_context() {
local role
if ! garage_split_cluster_contexts_enabled; then
return 0
fi
role="$(garage_context_role)"
[[ "$role" == "$GARAGE_AUTHORITY_ROLE" ]]
}
garage_ensure_absent_non_authoritative() {
local role
role="$(garage_context_role)"
echo "[INFO] Garage authority role is '${GARAGE_AUTHORITY_ROLE}'; current role '${role}' is non-authoritative. Ensuring Garage is absent in namespace '$NAMESPACE'."
delete_manifests || true
}
# Support both KNOE_HOME/k8s and sibling k8s directory
if [[ -d "$SCRIPT_DIR/../k8s/knoe" ]]; then
@ -67,11 +108,22 @@ GARAGE_FILES=(
"$GARAGE_MANIFEST_DIR/garage-service.yaml"
)
if [[ "${KNOE_MODE:-}" == "k3d" ]]; then
# k3d: no local storage provisioner — skip synology StorageClass and static PVs.
GARAGE_FILES=(
"$GARAGE_MANIFEST_DIR/garage-configmap.yaml"
"$GARAGE_MANIFEST_DIR/garage-statefulset.yaml"
"$GARAGE_MANIFEST_DIR/garage-service.yaml"
)
elif [[ "${KNOE_MODE:-}" == "k8s" ]]; then
# GKE Autopilot: apply our custom garage-hdd StorageClass (pd-standard HDD, avoids SSD quota).
# The skip-if-exists guard in apply_manifests handles idempotent re-runs safely.
# Use GCP-specific statefulset (no synology selectors, explicit resource requests).
GARAGE_FILES=(
"$GARAGE_MANIFEST_DIR/storageclass-gcp-hdd.yaml"
"$GARAGE_MANIFEST_DIR/garage-configmap.yaml"
"$GARAGE_MANIFEST_DIR/garage-statefulset-gcp.yaml"
"$GARAGE_MANIFEST_DIR/garage-service.yaml"
)
fi
GARAGE_APPLY_CHANGED=0
@ -160,9 +212,37 @@ apply_manifests() {
continue
fi
if [[ $diff_rc -ne 1 ]]; then
# For immutable StatefulSet VolumeClaimTemplates, kubectl diff itself returns an
# error (diff_rc != 1 but contains the immutable spec error). Handle it here.
if [[ "$(basename "$f")" == garage-statefulset*.yaml ]] \
&& echo "$diff_out" | grep -q "updates to statefulset spec"; then
echo "WARN: Garage StatefulSet VolumeClaimTemplates changed (diff-stage); deleting and recreating ..."
kubectl delete statefulset "$GARAGE_NAME" -n "$NAMESPACE" --ignore-not-found >/dev/null 2>&1 || true
local _pvc_sc_d
_pvc_sc_d=$(kubectl get pvc "data-garage-0" -n "$NAMESPACE" \
-o jsonpath='{.spec.storageClassName}' 2>/dev/null || true)
if [[ -n "$_pvc_sc_d" ]]; then
echo " Removing stale PVC 'data-garage-0' (storageClass: $_pvc_sc_d) ..."
kubectl delete pvc "data-garage-0" -n "$NAMESPACE" --ignore-not-found >/dev/null 2>&1 || true
fi
printf '%s' "$rendered" | kubectl apply --validate=false -n "$NAMESPACE" -f -
GARAGE_APPLY_CHANGED=1
GARAGE_STATEFULSET_CHANGED=1
continue
fi
echo "$diff_out" >&2
exit 1
fi
# StorageClass resources have immutable parameters/reclaimPolicy.
# If the StorageClass already exists in the cluster, skip re-applying it.
if echo "$rendered" | grep -q "^kind: StorageClass"; then
local _sc_name
_sc_name=$(echo "$rendered" | grep "^ name:" | head -1 | awk '{print $2}')
if [[ -n "$_sc_name" ]] && kubectl get storageclass "$_sc_name" >/dev/null 2>&1; then
echo "[SKIP] StorageClass '$_sc_name' already exists; skipping apply (immutable fields)."
continue
fi
fi
if output=$(printf '%s' "$rendered" | kubectl apply --validate=false -n "$NAMESPACE" -f - 2>&1); then
printf '%s\n' "$output"
GARAGE_APPLY_CHANGED=1
@ -175,6 +255,23 @@ apply_manifests() {
&& echo "$output" | grep -q "updates to statefulset spec"; then
echo "WARN: Garage StatefulSet immutable in k3d; skipping apply."
continue
elif [[ "$(basename "$f")" == garage-statefulset*.yaml ]] \
&& echo "$output" | grep -q "updates to statefulset spec"; then
# VolumeClaimTemplates are immutable; delete the StatefulSet (PVCs are orphaned/preserved)
# and recreate so the new storageClass name takes effect.
echo "WARN: Garage StatefulSet VolumeClaimTemplates changed; deleting and recreating ..."
kubectl delete statefulset "$GARAGE_NAME" -n "$NAMESPACE" --ignore-not-found >/dev/null 2>&1 || true
local _pvc_sc
_pvc_sc=$(kubectl get pvc "data-garage-0" -n "$NAMESPACE" \
-o jsonpath='{.spec.storageClassName}' 2>/dev/null || true)
if [[ -n "$_pvc_sc" ]]; then
echo " Removing stale PVC 'data-garage-0' (storageClass: $_pvc_sc) ..."
kubectl delete pvc "data-garage-0" -n "$NAMESPACE" --ignore-not-found >/dev/null 2>&1 || true
fi
printf '%s' "$rendered" | kubectl apply --validate=false -n "$NAMESPACE" -f -
GARAGE_APPLY_CHANGED=1
GARAGE_STATEFULSET_CHANGED=1
continue
fi
echo "$output" >&2
exit 1
@ -322,7 +419,11 @@ wait_ready() {
local initial_timeout="${PVC_REPAIR_WAIT_TIMEOUT:-30s}"
if ! kubectl rollout status statefulset/$GARAGE_NAME -n "$NAMESPACE" --timeout="$initial_timeout"; then
echo "WARN: Garage not ready after $initial_timeout; checking for Released synology-iscsi PVs with stale claimRefs ..." >&2
if [[ "${KNOE_MODE:-}" == "k8s" ]]; then
echo "WARN: Garage not ready after $initial_timeout; PVC provisioning may still be in progress (GKE CSI)." >&2
else
echo "WARN: Garage not ready after $initial_timeout; checking for Released synology-iscsi PVs with stale claimRefs ..." >&2
fi
recycle_released_knoe_iscsi_pv_for_pvc "$NAMESPACE" "data-garage-0" || true
fi
@ -421,6 +522,21 @@ status() {
case "$ACTION" in
start|initialize|update|reload|restart)
ensure_tools
if [[ "$GARAGE_AUTHORITY_ROLE" != "db" && "$GARAGE_AUTHORITY_ROLE" != "app" ]]; then
echo "ERROR: GARAGE_AUTHORITY_ROLE must be 'db' or 'app' (got '$GARAGE_AUTHORITY_ROLE')." >&2
exit 1
fi
if garage_split_cluster_contexts_enabled; then
current_role="$(garage_context_role)"
if [[ "$current_role" == "unknown" ]]; then
echo "ERROR: Unable to resolve Garage deployment cluster role from kubecontext '${ACTIVE_KUBE_CONTEXT:-<unset>}' (APP='${APP_CLUSTER_KUBECONTEXT:-<unset>}', DB='${DB_CLUSTER_KUBECONTEXT:-<unset>}')." >&2
exit 1
fi
if ! garage_is_authoritative_context; then
garage_ensure_absent_non_authoritative
exit 0
fi
fi
ensure_namespace
ensure_secrets
k3d_cleanup_pending_pvc

309
mock_val/init_gitea.sh Normal file → Executable file
View File

@ -20,7 +20,7 @@ Usage:
Options:
--mode <k3d|k3s|k8s|local> Deployment mode (default: ${MODE:-k3d})
-n, --namespace <name> Target namespace (default: gitea)
-c, --config <knoe.cfg> Path to knoe.cfg (defaults to detected)
-c, --config <config.cfg> Path to config file (defaults to detected)
--force Delete existing release before deploy
--help Show this help
@ -49,11 +49,12 @@ while [[ $# -gt 0 ]]; do
esac
done
# Resolve config path and namespace defaults from knoe.cfg when present
if [[ -z "$CFG_PATH" && -n "${KNOE_CONF:-}" && -f "${KNOE_CONF}/knoe.cfg" ]]; then
CFG_PATH="${KNOE_CONF}/knoe.cfg"
elif [[ -z "$CFG_PATH" && -f "$SCRIPT_DIR/../conf/knoe.cfg" ]]; then
CFG_PATH="$SCRIPT_DIR/../conf/knoe.cfg"
# Resolve config path and namespace defaults when present
if [[ -z "$CFG_PATH" && -n "${KNOE_CONF:-}" ]]; then
CFG_PATH="$(_knoe_cfg_select_cfg_file "${KNOE_CONF}")"
fi
if [[ -z "$CFG_PATH" ]]; then
CFG_PATH="$(_knoe_cfg_select_cfg_file "$SCRIPT_DIR/../conf")"
fi
if [[ -z "$NAMESPACE" && -n "$CFG_PATH" ]]; then
@ -81,6 +82,124 @@ RELEASE_NAME="gitea"
IMAGE_REPO_DEFAULT="gitea/gitea"
IMAGE_TAG="${GITEA_IMAGE_TAG:-1.22.3}"
IMAGE_REPO="$IMAGE_REPO_DEFAULT"
NODE_SELECTOR="${GITEA_NODE_SELECTOR:-${NODE_SELECTOR:-}}"
NODE_SELECTOR_KEY="${GITEA_NODE_SELECTOR_KEY:-${NODE_SELECTOR_KEY:-kubernetes.io/hostname}}"
GITEA_PV_NODE_SELECTOR_KEY="${GITEA_PV_NODE_SELECTOR_KEY:-${NODE_SELECTOR_KEY}}"
GITEA_PV_NODE="${GITEA_PV_NODE:-${GITEA_NODE_SELECTOR:-}}"
GITEA_PV_BASE_DIR="${GITEA_PV_BASE_DIR:-/synology/d005}"
GITEA_STORAGE_CLASS="${GITEA_STORAGE_CLASS:-gitea-local-d005}"
GITEA_DOMAIN="${GITEA_DOMAIN:-git.prole.org}"
GITEA_SSH_DOMAIN="${GITEA_SSH_DOMAIN:-$GITEA_DOMAIN}"
KNOE_DB_NAMESPACE="${KNOE_DB_NAMESPACE:-${DATABASE_NAMESPACE:-knoe-db}}"
KNOE_DB_CLUSTER="${KNOE_DB_CLUSTER:-${CLUSTER_NAME:-knoe-db}}"
KNOE_DB_SERVICE="${KNOE_DB_SERVICE:-${KNOE_DB_CLUSTER}-rw}"
KNOE_DB_PORT="${KNOE_DB_PORT:-${DB_HOST_PORT:-5432}}"
KNOE_DB_ADMIN_USER="${KNOE_DB_ADMIN_USER:-${KNOE_DB_USER:-postgres}}"
GITEA_DB_NAME="${GITEA_DB_NAME:-gitea}"
GITEA_DB_USER="${GITEA_DB_USER:-gitea}"
GITEA_DB_PASSWORD="${GITEA_DB_PASSWORD:-${DB_PASSWORD:-}}"
if [[ "$MODE" == "k3s" && -z "$GITEA_PV_NODE" ]]; then
die "GITEA_PV_NODE (or GITEA_NODE_SELECTOR) must be set in k3s mode."
fi
is_secret_placeholder() {
case "${1:-}" in
'${KNOE_SECRET:'*|'${OPENBAO:'*) return 0 ;;
esac
return 1
}
resolve_gitea_db_password() {
if [[ -n "${GITEA_DB_PASSWORD:-}" ]] && ! is_secret_placeholder "${GITEA_DB_PASSWORD}"; then
return 0
fi
if kubectl -n "$KNOE_DB_NAMESPACE" get secret knoe-db-superuser >/dev/null 2>&1; then
local resolved
resolved=$(kubectl -n "$KNOE_DB_NAMESPACE" get secret knoe-db-superuser \
-o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null || true)
if [[ -n "$resolved" ]]; then
GITEA_DB_PASSWORD="$resolved"
fi
fi
if [[ -z "${GITEA_DB_PASSWORD:-}" ]] || is_secret_placeholder "${GITEA_DB_PASSWORD}"; then
warn "Could not resolve a concrete Gitea DB password; database bootstrap may be skipped."
fi
}
resolve_knoe_db_primary_pod() {
local primary
primary=$(kubectl -n "$KNOE_DB_NAMESPACE" get pods \
-l "cnpg.io/cluster=${KNOE_DB_CLUSTER},cnpg.io/instanceRole=primary" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -z "$primary" ]]; then
primary=$(kubectl -n "$KNOE_DB_NAMESPACE" get pods \
-l "cnpg.io/cluster=${KNOE_DB_CLUSTER}" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
fi
printf '%s' "$primary"
}
sql_escape_literal() {
printf '%s' "${1:-}" | sed "s/'/''/g"
}
setup_knoe_db_for_gitea() {
local primary
primary="$(resolve_knoe_db_primary_pod)"
if [[ -z "$primary" ]]; then
warn "No knoe-db pod found in namespace '${KNOE_DB_NAMESPACE}'; skipping Gitea DB setup."
return 0
fi
resolve_gitea_db_password
if [[ -z "${GITEA_DB_PASSWORD:-}" ]] || is_secret_placeholder "${GITEA_DB_PASSWORD}"; then
warn "Skipping Gitea DB setup due to unresolved GITEA_DB_PASSWORD."
return 0
fi
local admin_user=""
local candidate
for candidate in "$KNOE_DB_ADMIN_USER" postgres root; do
[[ -z "$candidate" ]] && continue
if kubectl -n "$KNOE_DB_NAMESPACE" exec "$primary" -c postgres -- \
psql -U "$candidate" -d postgres -tAc "SELECT 1" >/dev/null 2>&1; then
admin_user="$candidate"
break
fi
done
if [[ -z "$admin_user" ]]; then
warn "Unable to connect to knoe-db as admin user; skipping Gitea DB setup."
return 0
fi
local escaped_password
escaped_password="$(sql_escape_literal "$GITEA_DB_PASSWORD")"
kubectl -n "$KNOE_DB_NAMESPACE" exec "$primary" -c postgres -- \
psql -U "$admin_user" -d postgres -c "
DO \$\$ BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='${GITEA_DB_USER}') THEN
CREATE ROLE ${GITEA_DB_USER} LOGIN PASSWORD '${escaped_password}';
END IF;
END \$\$;
" >/dev/null 2>&1 || warn "Could not create role '${GITEA_DB_USER}'."
local db_exists
db_exists=$(kubectl -n "$KNOE_DB_NAMESPACE" exec "$primary" -c postgres -- \
psql -U "$admin_user" -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname='${GITEA_DB_NAME}';" 2>/dev/null || true)
if [[ "$db_exists" != "1" ]]; then
kubectl -n "$KNOE_DB_NAMESPACE" exec "$primary" -c postgres -- \
psql -U "$admin_user" -d postgres -c "CREATE DATABASE ${GITEA_DB_NAME} OWNER ${GITEA_DB_USER};" \
>/dev/null 2>&1 || warn "Could not create database '${GITEA_DB_NAME}'."
fi
log "Gitea database '${GITEA_DB_NAME}' prepared in namespace '${KNOE_DB_NAMESPACE}'."
}
# Optional reset
if [[ "$FORCE" -eq 1 ]]; then
@ -119,20 +238,177 @@ deploy_with_helm() {
# Minimal config: NodePort or ClusterIP + Ingress depending on environment.
# Avoid heavy persistence defaults; users can override via values later.
local -a helm_args=(
--set "image.repository=$IMAGE_REPO"
--set "image.tag=$IMAGE_TAG"
--set "image.rootless=false"
--set "image.fullOverride=${IMAGE_REPO}:${IMAGE_TAG}"
--set "service.http.type=ClusterIP"
--set "service.ssh.type=ClusterIP"
--set "service.ssh.port=22"
--set "gitea.admin.username=${GITEA_ADMIN_USER:-gitea_admin}"
--set "gitea.admin.password=${GITEA_ADMIN_PASSWORD:-gitea_admin}"
--set "gitea.admin.email=${GITEA_ADMIN_EMAIL:-gitea_admin@example.local}"
--set "gitea.config.server.DOMAIN=${GITEA_DOMAIN}"
--set "gitea.config.server.SSH_DOMAIN=${GITEA_SSH_DOMAIN}"
--set "gitea.config.server.ROOT_URL=http://${GITEA_DOMAIN}/"
--set "gitea.config.server.HTTP_PORT=3000"
--set "gitea.config.server.SSH_PORT=22"
--set "gitea.config.database.DB_TYPE=postgres"
--set "gitea.config.database.HOST=${KNOE_DB_SERVICE}.${KNOE_DB_NAMESPACE}.svc.cluster.local:${KNOE_DB_PORT}"
--set "gitea.config.database.NAME=${GITEA_DB_NAME}"
--set "gitea.config.database.USER=${GITEA_DB_USER}"
--set-string "gitea.config.database.PASSWD=${GITEA_DB_PASSWORD}"
--set "gitea.config.database.SSL_MODE=disable"
--set "gitea.config.session.PROVIDER=db"
--set "postgresql.enabled=false"
--set "postgresql-ha.enabled=false"
--set "valkey-cluster.enabled=false"
--set "persistence.enabled=true"
)
if [[ "$MODE" == "k3s" ]]; then
helm_args+=(
--set "gitea.config.cache.ADAPTER=memory"
--set "global.storageClass=$GITEA_STORAGE_CLASS"
--set "persistence.storageClass=$GITEA_STORAGE_CLASS"
--set-json "nodeSelector={\"${GITEA_PV_NODE_SELECTOR_KEY}\":\"${GITEA_PV_NODE}\"}"
--set "deployment.strategy=Recreate"
)
fi
helm upgrade --install "$RELEASE_NAME" "$CHART_NAME" \
-n "$NAMESPACE" \
--set image.repository="$IMAGE_REPO" \
--set image.tag="$IMAGE_TAG" \
--set service.http.type=ClusterIP \
--set service.ssh.type=ClusterIP \
--set gitea.admin.username="${GITEA_ADMIN_USER:-gitea_admin}" \
--set gitea.admin.password="${GITEA_ADMIN_PASSWORD:-gitea_admin}" \
--set gitea.admin.email="${GITEA_ADMIN_EMAIL:-gitea_admin@example.local}" \
"${helm_args[@]}" \
--wait --timeout 10m
}
ensure_k3s_storage_layout() {
[[ "$MODE" == "k3s" ]] || return 0
log "Ensuring Gitea k3s storage on node '${GITEA_PV_NODE}' at '${GITEA_PV_BASE_DIR}' (storageClass=${GITEA_STORAGE_CLASS})"
cat <<EOF | kubectl apply -f -
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ${GITEA_STORAGE_CLASS}
provisioner: kubernetes.io/no-provisioner
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
EOF
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolume
metadata:
name: gitea-d005-shared-storage
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
storageClassName: ${GITEA_STORAGE_CLASS}
persistentVolumeReclaimPolicy: Retain
hostPath:
path: ${GITEA_PV_BASE_DIR}/gitea/shared
type: DirectoryOrCreate
claimRef:
namespace: ${NAMESPACE}
name: gitea-shared-storage
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: ${GITEA_PV_NODE_SELECTOR_KEY}
operator: In
values:
- ${GITEA_PV_NODE}
EOF
}
prepare_k3s_host_permissions() {
[[ "$MODE" == "k3s" ]] || return 0
local prep_pod="gitea-storage-perms"
kubectl -n "$NAMESPACE" delete pod "$prep_pod" --ignore-not-found >/dev/null 2>&1 || true
cat <<EOF | kubectl apply -n "$NAMESPACE" -f -
apiVersion: v1
kind: Pod
metadata:
name: ${prep_pod}
spec:
restartPolicy: Never
nodeSelector:
${GITEA_PV_NODE_SELECTOR_KEY}: ${GITEA_PV_NODE}
containers:
- name: perms
image: busybox:1.36
command:
- /bin/sh
- -c
args:
- |
set -eu
mkdir -p \
/host/gitea/shared
chmod -R 0777 /host/gitea
securityContext:
runAsUser: 0
volumeMounts:
- name: host-root
mountPath: /host
volumes:
- name: host-root
hostPath:
path: ${GITEA_PV_BASE_DIR}
type: DirectoryOrCreate
EOF
kubectl -n "$NAMESPACE" wait --for=jsonpath='{.status.phase}'=Succeeded pod/${prep_pod} --timeout=120s >/dev/null
kubectl -n "$NAMESPACE" delete pod "$prep_pod" --ignore-not-found >/dev/null 2>&1 || true
}
reset_stuck_k3s_release() {
[[ "$MODE" == "k3s" ]] || return 0
local pending
pending=$(kubectl -n "$NAMESPACE" get pvc -o jsonpath='{range .items[*]}{.spec.storageClassName}:{.status.phase}{"\n"}{end}' 2>/dev/null | grep '^:Pending$' || true)
if [[ -z "$pending" ]]; then
return 0
fi
warn "Detected pending PVCs without storageClass in namespace ${NAMESPACE}; resetting stuck Gitea release"
if command -v helm >/dev/null 2>&1; then
helm uninstall "$RELEASE_NAME" -n "$NAMESPACE" >/dev/null 2>&1 || true
fi
kubectl -n "$NAMESPACE" delete pvc gitea-shared-storage data-gitea-postgresql-ha-postgresql-0 data-gitea-postgresql-ha-postgresql-1 data-gitea-postgresql-ha-postgresql-2 valkey-data-gitea-valkey-cluster-0 valkey-data-gitea-valkey-cluster-1 valkey-data-gitea-valkey-cluster-2 >/dev/null 2>&1 || true
}
release_exists() {
command -v helm >/dev/null 2>&1 && helm status "$RELEASE_NAME" -n "$NAMESPACE" >/dev/null 2>&1
}
gitea_workload_exists() {
kubectl -n "$NAMESPACE" get deploy "$RELEASE_NAME" >/dev/null 2>&1
}
apply_manifest_fallback() {
warn "Helm unavailable; applying fallback manifest"
if release_exists || gitea_workload_exists; then
warn "Helm resources detected for release '$RELEASE_NAME'; skipping fallback manifest to avoid immutable selector/port conflicts"
return 0
fi
warn "Applying fallback manifest"
local node_selector_block=""
if [[ -n "$NODE_SELECTOR" ]]; then
node_selector_block=$(cat <<EOF
nodeSelector:
${NODE_SELECTOR_KEY}: ${NODE_SELECTOR}
EOF
)
fi
cat <<EOF | kubectl apply -n "$NAMESPACE" -f -
apiVersion: apps/v1
kind: Deployment
@ -148,6 +424,7 @@ spec:
labels:
app: gitea
spec:
${node_selector_block}
containers:
- name: gitea
image: ${IMAGE_REPO}:${IMAGE_TAG}
@ -193,6 +470,10 @@ case "$ACTION" in
esac
log "Deploying Gitea to namespace '$NAMESPACE' (mode=$MODE)"
reset_stuck_k3s_release
ensure_k3s_storage_layout
prepare_k3s_host_permissions
setup_knoe_db_for_gitea
if deploy_with_helm; then
log "Gitea deployed via Helm"
else

4767
mock_val/init_gitlab.sh Normal file → Executable file

File diff suppressed because it is too large Load Diff

99
mock_val/init_grafana_oauth.sh Executable file
View File

@ -0,0 +1,99 @@
#!/usr/bin/env bash
# init_grafana_oauth.sh
#
# Bootstrap the Google-OAuth secret that kps-grafana mounts as env vars
# (GF_AUTH_GOOGLE_CLIENT_ID / GF_AUTH_GOOGLE_CLIENT_SECRET) for its native
# auth.google sign-in flow. Companion to:
# - deploy/gcp/gke/grafana-google-oidc-secret.example.yaml (envsubst template)
# - monitoring/kps-values-gke.yaml (Helm overrides for grafana subchart)
#
# Auth model:
# - Anyone in the @knoey.com Workspace can sign in (auth.google.allowed_domains).
# - chrisfu@knoey.com + ron@knoey.com get Admin (role_attribute_path JMESPath).
# - Everyone else @knoey.com gets Editor.
#
# Usage:
# ./etc/init_grafana_oauth.sh
#
# Env vars (resolved from etc/secrets/* if not set in the shell):
# GRAFANA_GOOGLE_CLIENT_ID ← from etc/secrets/grafana-google-oidc-client-id
# GRAFANA_GOOGLE_CLIENT_SECRET ← from etc/secrets/grafana-google-oidc-client-secret
#
# Optional:
# APP_CLUSTER_KUBECONTEXT (default: $KUBECONTEXT then ambient)
# NAMESPACE (default: monitoring)
#
# Pre-reqs:
# - OAuth 2.0 client created at GCP Console (see the secret template
# deploy/gcp/gke/grafana-google-oidc-secret.example.yaml for the
# exact authorized redirect URI + consent screen settings).
# - Two values saved into etc/secrets/grafana-google-oidc-client-{id,secret}
# (chmod 0600 each; etc/secrets/ is gitignored except for .keep).
#
# After this script runs and the Secret is in place, the next `helm upgrade`
# (or kubectl-apply of the chart's rendered manifest) of kps-grafana picks
# up the Secret via `envFromSecret: grafana-google-oidc`. Verify with:
# kubectl --context=$APP_CLUSTER_KUBECONTEXT -n monitoring exec -it kps-grafana-0 -c grafana -- \
# env | grep GF_AUTH_GOOGLE_
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"
NAMESPACE="${NAMESPACE:-monitoring}"
KCTX="${APP_CLUSTER_KUBECONTEXT:-${KUBECONTEXT:-}}"
if [[ -n "$KCTX" ]]; then
KCTX_FLAG=(--context="$KCTX")
else
KCTX_FLAG=()
fi
log() { printf "[%s] %s\n" "$(date +%H:%M:%S)" "$*"; }
die() { log "ERROR: $*" >&2; exit 1; }
resolve_secret() {
local var="$1" file="$2" val="${!1:-}"
if [[ -z "$val" && -f "$REPO_ROOT/etc/secrets/$file" ]]; then
val="$(cat "$REPO_ROOT/etc/secrets/$file")"
fi
if [[ -z "$val" ]]; then
die "missing $var (set the env var, or save the value into etc/secrets/$file)"
fi
printf '%s' "$val"
}
for tool in kubectl envsubst; do
command -v "$tool" >/dev/null 2>&1 || die "required tool not found: $tool"
done
GRAFANA_GOOGLE_CLIENT_ID="$(resolve_secret GRAFANA_GOOGLE_CLIENT_ID grafana-google-oidc-client-id)"
GRAFANA_GOOGLE_CLIENT_SECRET="$(resolve_secret GRAFANA_GOOGLE_CLIENT_SECRET grafana-google-oidc-client-secret)"
export GRAFANA_GOOGLE_CLIENT_ID GRAFANA_GOOGLE_CLIENT_SECRET
SECRET_TMPL="$GKE_MANIFEST_DIR/grafana-google-oidc-secret.example.yaml"
[[ -f "$SECRET_TMPL" ]] || die "missing manifest: $SECRET_TMPL"
log "==> grafana google-oauth bootstrap"
log " namespace : $NAMESPACE"
log " kubectx : ${KCTX:-<ambient>}"
log "Applying grafana-google-oidc Secret ..."
envsubst '${GRAFANA_GOOGLE_CLIENT_ID} ${GRAFANA_GOOGLE_CLIENT_SECRET}' \
< "$SECRET_TMPL" \
| kubectl "${KCTX_FLAG[@]}" -n "$NAMESPACE" apply -f -
log "==> grafana google-oauth secret applied."
echo ""
echo " Next steps:"
echo " 1. Apply the Helm values override at monitoring/kps-values-gke.yaml:"
echo " helm upgrade --reuse-values kps prometheus-community/kube-prometheus-stack \\"
echo " --namespace $NAMESPACE \\"
echo " -f $REPO_ROOT/monitoring/kps-values-gke.yaml"
echo " 2. Patch knoe-svc-kong-config to add the /grafana route (see"
echo " deploy/opentofu/k3s/manifests/knoe/kong-configmap.yaml for the canonical source)."
echo " 3. Restart Kong: kubectl rollout restart deployment/knoe-svc-kong -n knoe-system"
echo " 4. Browser-test: https://svc.knoe.dev/grafana → Google sign-in → Grafana"
echo ""

View File

@ -0,0 +1,100 @@
#!/usr/bin/env bash
# init_grafana_oauth_prole.sh
#
# Bootstrap the Google-OAuth secret for Grafana on the prole.org k3s homelab
# cluster. Companion to the knoe.dev version (init_grafana_oauth.sh) but uses
# prole.org GCP project credentials and targets the k3s kubecontext.
#
# Creates the grafana-google-oidc Secret in the monitoring namespace, which
# kps-grafana mounts via envFromSecret to get GF_AUTH_GOOGLE_CLIENT_ID and
# GF_AUTH_GOOGLE_CLIENT_SECRET for its auth.google sign-in flow.
#
# Auth model (monitoring/kps-values-k3s.yaml):
# - Kerberos/knoe-auth users auto-login via auth.proxy (X-WEBAUTH-USER).
# - chrisfu@prole.org (and any future @prole.org Workspace user) signs in
# with the Google button on the Grafana login page.
# - chrisfu@prole.org → Admin; all other @prole.org users → Editor.
#
# Usage:
# ./etc/init_grafana_oauth_prole.sh
#
# Env vars (resolved from etc/secrets/* if not set in the shell):
# GRAFANA_GOOGLE_CLIENT_ID ← from etc/secrets/grafana-google-oidc-client-id-prole
# GRAFANA_GOOGLE_CLIENT_SECRET ← from etc/secrets/grafana-google-oidc-client-secret-prole
#
# Optional:
# K3S_KUBECONTEXT (default: $KUBECONTEXT then ambient)
# NAMESPACE (default: monitoring)
#
# Pre-reqs:
# - OAuth 2.0 Web Application client created in the prole.org GCP project:
# Authorized JS origins: https://svc.prole.org
# Authorized redirect URI: https://svc.prole.org/grafana/login/google
# Consent screen: Internal (prole.org Workspace)
# Scopes: openid, email, profile
# See deploy/gcp/gke/grafana-google-oidc-secret-prole.example.yaml for details.
# - Client ID and secret saved (chmod 0600) to:
# etc/secrets/grafana-google-oidc-client-id-prole
# etc/secrets/grafana-google-oidc-client-secret-prole
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
MANIFEST_DIR="$REPO_ROOT/deploy/gcp/gke"
NAMESPACE="${NAMESPACE:-monitoring}"
KCTX="${K3S_KUBECONTEXT:-${KUBECONTEXT:-}}"
if [[ -n "$KCTX" ]]; then
KCTX_FLAG=(--context="$KCTX")
else
KCTX_FLAG=()
fi
log() { printf "[%s] %s\n" "$(date +%H:%M:%S)" "$*"; }
die() { log "ERROR: $*" >&2; exit 1; }
resolve_secret() {
local var="$1" file="$2" val="${!1:-}"
if [[ -z "$val" && -f "$REPO_ROOT/etc/secrets/$file" ]]; then
val="$(cat "$REPO_ROOT/etc/secrets/$file")"
fi
if [[ -z "$val" ]]; then
die "missing $var (set the env var, or save the value into etc/secrets/$file)"
fi
printf '%s' "$val"
}
for tool in kubectl envsubst; do
command -v "$tool" >/dev/null 2>&1 || die "required tool not found: $tool"
done
GRAFANA_GOOGLE_CLIENT_ID="$(resolve_secret GRAFANA_GOOGLE_CLIENT_ID grafana-google-oidc-client-id-prole)"
GRAFANA_GOOGLE_CLIENT_SECRET="$(resolve_secret GRAFANA_GOOGLE_CLIENT_SECRET grafana-google-oidc-client-secret-prole)"
export GRAFANA_GOOGLE_CLIENT_ID GRAFANA_GOOGLE_CLIENT_SECRET
SECRET_TMPL="$MANIFEST_DIR/grafana-google-oidc-secret-prole.example.yaml"
[[ -f "$SECRET_TMPL" ]] || die "missing manifest: $SECRET_TMPL"
log "==> grafana google-oauth bootstrap (prole.org / k3s)"
log " namespace : $NAMESPACE"
log " kubectx : ${KCTX:-<ambient>}"
log "Applying grafana-google-oidc Secret ..."
envsubst '${GRAFANA_GOOGLE_CLIENT_ID} ${GRAFANA_GOOGLE_CLIENT_SECRET}' \
< "$SECRET_TMPL" \
| kubectl "${KCTX_FLAG[@]}" -n "$NAMESPACE" apply -f -
log "==> grafana google-oauth secret applied."
echo ""
echo " Next steps:"
echo " 1. Helm upgrade kube-prometheus-stack with the k3s values:"
echo " helm upgrade --reuse-values kps prometheus-community/kube-prometheus-stack \\"
echo " --namespace $NAMESPACE \\"
echo " -f $REPO_ROOT/monitoring/kps-values-k3s.yaml"
echo " 2. Restart grafana-proxy to pick up the updated configmap:"
echo " kubectl rollout restart deployment/knoe-grafana-proxy -n $NAMESPACE"
echo " 3. Browser-test: https://svc.prole.org/grafana/login → Google sign-in button"
echo " present; Kerberos users still auto-login via X-WEBAUTH-USER."
echo ""

View File

@ -22,11 +22,17 @@ registry_host_from_url() {
printf '%s' "$value"
}
K3S_REGISTRY_HOST=${K3S_REGISTRY_HOST:-$(registry_host_from_url "${PROLE_K3S_SERVER:-${K3S_SERVER_URL:-}}")}
K3S_REGISTRY_HOST=${K3S_REGISTRY_HOST:-$(registry_host_from_url "${PROLE_K3S_SERVER:-${K3S_SERVER_URL:-}}")}
K3S_REGISTRY_PORT=${K3S_REGISTRY_PORT:-5000}
K3S_REGISTRY_NAMESPACE=${K3S_REGISTRY_NAMESPACE:-${REGISTRY_NAMESPACE:-${SERVICE_NAMESPACE:-${PROLE_NAMESPACE:-default}}}}
K3S_REGISTRY_FILE=${K3S_REGISTRY_FILE:-/etc/rancher/k3s/registries.yaml}
K3S_REGISTRY_SCHEME=${K3S_REGISTRY_SCHEME:-http}
K3S_REGISTRY_SCHEME=${K3S_REGISTRY_SCHEME:-}
if [[ -z "${K3S_REGISTRY_SCHEME}" ]]; then
# Internal Knoe registry endpoints are plain HTTP by default.
# Set K3S_REGISTRY_SCHEME=https explicitly when TLS is configured.
K3S_REGISTRY_SCHEME="http"
fi
ensure_root() {
if [[ "$(id -u)" -ne 0 ]]; then
@ -40,6 +46,7 @@ render_registries_yaml() {
local port="$2"
local ns="$3"
local scheme="$4"
if [[ "$scheme" == "http" ]]; then
cat <<EOF
mirrors:

View File

@ -26,13 +26,44 @@ elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
fi
knoe_ensure_kubeconfig >/dev/null 2>&1 || true
knoe_ensure_kube_context || exit 1
ensure_kube_context || exit 1
ACTION=${1:-initialize}
resolve_kdc_mode_hint() {
local mode_hint="${KNOE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-k3s}}}"
if command -v knoe_normalize_mode >/dev/null 2>&1; then
knoe_normalize_mode "$mode_hint"
return 0
fi
mode_hint=$(printf '%s' "$mode_hint" | tr 'A-Z' 'a-z')
case "$mode_hint" in
prod|production)
printf 'k8s'
;;
*)
printf '%s' "$mode_hint"
;;
esac
}
default_knoe_kdc_name() {
case "$(resolve_kdc_mode_hint)" in
k8s)
printf 'authority-gcp-auth'
;;
*)
printf 'authority-knoe-auth'
;;
esac
}
KDC_NAMESPACE=${PROLE_KDC_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-default}}}
# Namespace where knoe-auth runs and expects the `knoe-kdc-config` ConfigMap.
# Defaults to PROLE_NAMESPACE (from knoe.cfg) when present.
PROLE_AUTH_NAMESPACE=${PROLE_AUTH_NAMESPACE:-${PROLE_NAMESPACE:-}}
PROLE_KDC_ENABLED=${PROLE_KDC_ENABLED:-1}
PROLE_KDC_NAME=${PROLE_KDC_NAME:-auth}
PROLE_KDC_NAME=${PROLE_KDC_NAME:-$(default_knoe_kdc_name)}
PROLE_KDC_SERVICE=${PROLE_KDC_SERVICE:-auth}
PROLE_KDC_IMAGE=${PROLE_KDC_IMAGE:-}
# If the user provided an explicit image, we should not require Docker unless
@ -293,7 +324,11 @@ get_secret_value() {
resolve_knoe_kdc_defaults() {
if [[ -z "$PROLE_KDC_REALM" ]]; then
PROLE_KDC_REALM="PROLE.LOCAL"
# Post-rebrand default — must match knoe-db/etc/init_kdc.sh. The old
# "PROLE.LOCAL" string seeded a stale KDC config on the prole k3s
# cluster that took an afternoon of cross-realm trust debugging to
# find — see ~/.claude/plans/chrisfu-myrddin-dev-prole-git-pull-*.md.
PROLE_KDC_REALM="KNOE.LOCAL"
fi
if [[ -z "$PROLE_KDC_DOMAIN" ]]; then
PROLE_KDC_DOMAIN=$(lowercase "$PROLE_KDC_REALM")
@ -532,6 +567,13 @@ EOF
host_net_block=" hostNetwork: true"
dns_policy_block=" dnsPolicy: ClusterFirstWithHostNet"
fi
# PVC storage class: explicit when $PROLE_KDC_STORAGE_CLASS is set;
# otherwise leave blank so the cluster's default StorageClass picks
# the binder (k3s "local-path", GKE "standard", etc.).
local storage_class_block=""
if [[ -n "${PROLE_KDC_STORAGE_CLASS:-}" ]]; then
storage_class_block=" storageClassName: ${PROLE_KDC_STORAGE_CLASS}"
fi
cat <<EOF | kubectl apply -n "$KDC_NAMESPACE" -f -
apiVersion: v1
kind: ConfigMap
@ -661,18 +703,42 @@ data:
if [[ -n "\${PROLE_KDC_TRUST_REALM:-}" && "\${PROLE_KDC_TRUST_REALM}" != "\${PROLE_KDC_REALM}" ]]; then
shared_pw="\${PROLE_KDC_TRUST_SHARED_PASSWORD:-\${PROLE_KDC_MASTER_PASSWORD}}"
# ------------------------------------------------------------------
# Cross-realm krbtgt principals — RC4 only.
#
# Both directions of the trust live as their own krbtgt principal,
# each keyed to the same shared password. We pin RC4 (arcfour-hmac)
# because AES key derivation requires a salt, and Samba's salt
# convention (<remote_realm> + UPN) does not match MIT's
# (<local_realm> + <principal-no-realm>). RC4 derives keys from
# the password alone, so both sides converge with no salt fight.
# ------------------------------------------------------------------
# Outbound: KNOE.LOCAL → PROLE.ORG (issued here, decrypted by Samba)
if ! kadmin.local -q "get_principal krbtgt/\${PROLE_KDC_TRUST_REALM}@\${PROLE_KDC_REALM}" >/dev/null 2>&1; then
echo "Creating trust principal krbtgt/\${PROLE_KDC_TRUST_REALM}@\${PROLE_KDC_REALM}..."
kadmin.local -q "addprinc -pw \${shared_pw} krbtgt/\${PROLE_KDC_TRUST_REALM}@\${PROLE_KDC_REALM}"
echo "Creating outbound trust principal krbtgt/\${PROLE_KDC_TRUST_REALM}@\${PROLE_KDC_REALM}..."
kadmin.local -q "addprinc -pw \${shared_pw} -e arcfour-hmac:normal krbtgt/\${PROLE_KDC_TRUST_REALM}@\${PROLE_KDC_REALM}"
fi
if [[ -n "\${PROLE_KDC_TRUST_ADMIN:-}" && -n "\${PROLE_KDC_TRUST_PASSWORD:-}" ]]; then
echo "Creating reciprocal trust principal in \${PROLE_KDC_TRUST_REALM}..."
kadmin -r "\${PROLE_KDC_TRUST_REALM}" -p "\${PROLE_KDC_TRUST_ADMIN}" -w "\${PROLE_KDC_TRUST_PASSWORD}" \
-q "addprinc -pw \${shared_pw} krbtgt/\${PROLE_KDC_REALM}@\${PROLE_KDC_TRUST_REALM}" || true
else
echo "WARN: Missing PROLE_KDC_TRUST_ADMIN/PROLE_KDC_TRUST_PASSWORD; skipping external trust principal."
# Inbound: PROLE.ORG → KNOE.LOCAL (issued by Samba, decrypted here)
if ! kadmin.local -q "get_principal krbtgt/\${PROLE_KDC_REALM}@\${PROLE_KDC_TRUST_REALM}" >/dev/null 2>&1; then
echo "Creating inbound trust principal krbtgt/\${PROLE_KDC_REALM}@\${PROLE_KDC_TRUST_REALM}..."
kadmin.local -q "addprinc -pw \${shared_pw} -e arcfour-hmac:normal krbtgt/\${PROLE_KDC_REALM}@\${PROLE_KDC_TRUST_REALM}"
fi
# NOTE: The Samba-side trust account (user "krbtgt_\${PROLE_KDC_REALM}"
# in PROLE.ORG with UPN/SPN krbtgt/\${PROLE_KDC_REALM}) is provisioned
# OUT-OF-BAND by this repo's Ansible playbook:
# infrastructure/playbooks/kerberos_trust_setup.yml
# Earlier versions of this script tried to use a remote "kadmin"
# client to write that principal into Samba, but Samba AD does not
# accept additions over MIT's kadmin protocol — it always failed
# with "Missing parameters in krb5.conf required for kadmin client".
# Run the playbook once after this KDC comes up:
# ANSIBLE_VAULT_PASSWORD_FILE=\$PWD/.vault_pass \\
# ansible-playbook infrastructure/playbooks/kerberos_trust_setup.yml
echo "Note: Samba-side trust account is provisioned out-of-band by"
echo " infrastructure/playbooks/kerberos_trust_setup.yml"
fi
# Start daemons. Keep kadmind in PID 1; run krb5kdc in background and verify it binds.
@ -774,7 +840,21 @@ ${dns_policy_block}
configMap:
name: knoe-kdc-config
- name: knoe-kdc-data
emptyDir: {}
persistentVolumeClaim:
claimName: knoe-kdc-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: knoe-kdc-data
namespace: ${KDC_NAMESPACE}
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: ${PROLE_KDC_STORAGE_SIZE:-1Gi}
${storage_class_block}
---
apiVersion: v1
kind: Service
@ -810,6 +890,112 @@ EOF
apply_kdc_manifest
# knoe-auth runs in PROLE_AUTH_NAMESPACE (default: PROLE_NAMESPACE) and mounts
# ConfigMap `knoe-kdc-config` for its embedded KDC sidecar. When the KDC itself
# is deployed into a different namespace (default: SERVICE_NAMESPACE), ensure
# the configmap also exists in the knoe-auth namespace to prevent FailedMount.
if [[ -n "${PROLE_AUTH_NAMESPACE:-}" && "${PROLE_AUTH_NAMESPACE}" != "${KDC_NAMESPACE}" ]]; then
if ! kubectl get namespace "$PROLE_AUTH_NAMESPACE" >/dev/null 2>&1; then
log "Creating namespace '$PROLE_AUTH_NAMESPACE' ..."
kubectl create namespace "$PROLE_AUTH_NAMESPACE" >/dev/null 2>&1 || true
fi
log "Ensuring ConfigMap 'knoe-kdc-config' exists in namespace '${PROLE_AUTH_NAMESPACE}' for knoe-auth ..."
cat <<EOF | kubectl apply -n "$PROLE_AUTH_NAMESPACE" -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: knoe-kdc-config
namespace: ${PROLE_AUTH_NAMESPACE}
data:
krb5.conf: |
[libdefaults]
default_realm = ${PROLE_KDC_REALM}
dns_lookup_realm = false
dns_lookup_kdc = false
[realms]
${PROLE_KDC_REALM} = {
kdc = 127.0.0.1
admin_server = 127.0.0.1
}${trust_block}
kdc.conf: |
[kdcdefaults]
kdc_ports = 88
kdc_tcp_ports = 88
[realms]
${PROLE_KDC_REALM} = {
database_name = /var/lib/krb5kdc/principal
admin_keytab = FILE:/etc/krb5kdc/kadm5.keytab
acl_file = /etc/krb5kdc/kadm5.acl
key_stash_file = /etc/krb5kdc/stash
max_life = 10h 0m 0s
max_renewable_life = 7d 0h 0m 0s
default_principal_flags = +preauth
}
kadm5.acl: |
${admin_acl_principal} *
entrypoint.sh: |
#!/usr/bin/env bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
realm="\${PROLE_KDC_REALM:-PROLE.ORG}"
admin_principal="\${PROLE_KDC_ADMIN_PRINCIPAL:-admin/admin}"
if [[ "\${admin_principal}" != *"@"* ]]; then
admin_principal="\${admin_principal}@\${realm}"
fi
if ! command -v krb5kdc >/dev/null 2>&1; then
echo "Installing Kerberos packages..."
echo "krb5-config krb5-config/default_realm string \${PROLE_KDC_REALM}" | debconf-set-selections || true
echo "krb5-config krb5-config/kerberos_servers string 127.0.0.1" | debconf-set-selections || true
echo "krb5-config krb5-config/admin_server string 127.0.0.1" | debconf-set-selections || true
apt-get update
apt-get install -y --no-install-recommends krb5-kdc krb5-admin-server krb5-user dnsutils ca-certificates
rm -rf /var/lib/apt/lists/*
fi
mkdir -p /etc/krb5kdc /var/lib/krb5kdc
if [[ -f /opt/knoe-kdc/krb5.conf ]]; then
cp /opt/knoe-kdc/krb5.conf /etc/krb5.conf
fi
if [[ -f /opt/knoe-kdc/kdc.conf ]]; then
cp /opt/knoe-kdc/kdc.conf /etc/krb5kdc/kdc.conf
fi
if [[ -f /opt/knoe-kdc/kadm5.acl ]]; then
cp /opt/knoe-kdc/kadm5.acl /etc/krb5kdc/kadm5.acl
fi
if [[ -z "\${PROLE_KDC_MASTER_PASSWORD:-}" ]]; then
echo "ERROR: Missing required env PROLE_KDC_MASTER_PASSWORD (secret 'knoe-kdc-secrets/master_password')." >&2
exit 1
fi
if [[ -z "\${PROLE_KDC_ADMIN_PASSWORD:-}" ]]; then
echo "ERROR: Missing required env PROLE_KDC_ADMIN_PASSWORD (secret 'knoe-kdc-secrets/admin_password')." >&2
exit 1
fi
if [[ ! -f /var/lib/krb5kdc/principal ]]; then
echo "Initializing realm database for \${realm}..."
kdb5_util create -s -r "\${realm}" -P "\${PROLE_KDC_MASTER_PASSWORD}"
fi
if ! kadmin.local -q "get_principal \${admin_principal}" >/dev/null 2>&1; then
echo "Creating admin principal \${admin_principal}..."
kadmin.local -q "addprinc -pw \${PROLE_KDC_ADMIN_PASSWORD} \${admin_principal}"
fi
echo "Starting krb5kdc and kadmind ..."
krb5kdc -n &
sleep 0.5
if ! pgrep -x krb5kdc >/dev/null 2>&1; then
echo "ERROR: krb5kdc failed to start. Check /var/log/ (syslog) for details." >&2
exit 1
fi
exec kadmind -nofork
EOF
fi
local rollout_timeout="$PROLE_KDC_ROLLOUT_TIMEOUT"
if [[ "$deployment_present" -eq 0 ]]; then
rollout_timeout="$PROLE_KDC_DEPLOY_TIMEOUT"
@ -839,6 +1025,10 @@ cleanup_knoe_kdc() {
kubectl -n "$KDC_NAMESPACE" delete service "$PROLE_KDC_SERVICE" --ignore-not-found
kubectl -n "$KDC_NAMESPACE" delete deployment "$PROLE_KDC_NAME" --ignore-not-found
kubectl -n "$KDC_NAMESPACE" delete configmap knoe-kdc-config --ignore-not-found
if [[ -n "${PROLE_AUTH_NAMESPACE:-}" && "${PROLE_AUTH_NAMESPACE}" != "${KDC_NAMESPACE}" ]]; then
kubectl -n "$PROLE_AUTH_NAMESPACE" delete configmap knoe-kdc-config --ignore-not-found
fi
}
status() {

View File

@ -48,7 +48,7 @@ KRB5_AD_PROXY_IMAGE=${KRB5_AD_PROXY_IMAGE:-alpine/socat}
KRB5_AD_PROXY_HOST_NETWORK=${KRB5_AD_PROXY_HOST_NETWORK:-1}
KRB5_AD_TCP_PORTS=${KRB5_AD_TCP_PORTS:-"88 389 445 464 636"}
KRB5_AD_UDP_PORTS=${KRB5_AD_UDP_PORTS:-"88 464"}
CNPG_WAIT_TIMEOUT=${CNPG_WAIT_TIMEOUT:-300}
CNPG_WAIT_TIMEOUT=${CNPG_WAIT_TIMEOUT:-900}
SERVICE_NAMESPACE=${SERVICE_NAMESPACE:-${NAMESPACE:-default}}
OPENBAO_NAMESPACE=${OPENBAO_NAMESPACE:-${SERVICE_NAMESPACE:-default}}
KRB5_AD_NAMESPACE=${KRB5_AD_NAMESPACE:-${SERVICE_NAMESPACE}}
@ -92,6 +92,12 @@ wait_for_ad_forwarder_ready() {
}
ensure_tools() {
if [[ "${KNOE_MODE:-}" == "min" ]]; then
for t in curl jq; do
command -v "$t" >/dev/null || { err "Missing required tool: $t"; exit 1; }
done
return
fi
for t in kubectl curl jq; do
command -v "$t" >/dev/null || { err "Missing required tool: $t"; exit 1; }
done
@ -156,12 +162,24 @@ is_ip_address() {
}
detect_knoe_cfg() {
if [[ -n "${KNOE_CONF:-}" && -f "$KNOE_CONF/knoe.cfg" ]]; then
printf '%s' "$KNOE_CONF/knoe.cfg"
elif [[ -n "${KNOE_HOME:-}" && -f "$KNOE_HOME/conf/knoe.cfg" ]]; then
printf '%s' "$KNOE_HOME/conf/knoe.cfg"
elif [[ -f "$SCRIPT_DIR/../conf/knoe.cfg" ]]; then
printf '%s' "$SCRIPT_DIR/../conf/knoe.cfg"
local cfg=""
if [[ -n "${KNOE_CONF:-}" ]]; then
cfg="$(_knoe_cfg_select_cfg_file "$KNOE_CONF")"
if [[ -n "$cfg" ]]; then
printf '%s' "$cfg"
return 0
fi
fi
if [[ -n "${KNOE_HOME:-}" ]]; then
cfg="$(_knoe_cfg_select_cfg_file "$KNOE_HOME/conf")"
if [[ -n "$cfg" ]]; then
printf '%s' "$cfg"
return 0
fi
fi
cfg="$(_knoe_cfg_select_cfg_file "$SCRIPT_DIR/../conf")"
if [[ -n "$cfg" ]]; then
printf '%s' "$cfg"
fi
}
@ -481,6 +499,11 @@ default_port_forward_if_local() {
}
sync_knoe_kdc_trust() {
if [[ "${PROLE_KDC_STANDALONE:-0}" != "1" ]]; then
# Default deployment embeds the KDC as a sidecar in `knoe-auth`; do not
# attempt to manage a standalone KDC unless explicitly requested.
return 0
fi
if [[ ! -x "$SCRIPT_DIR/init_kdc.sh" ]]; then
return 0
fi
@ -952,6 +975,10 @@ maybe_apply_k3d_route_fix() {
}
initialize() {
if [[ "${KNOE_MODE:-}" == "min" ]]; then
log "Minimal mode: skipping Kerberos Kubernetes resource initialization."
return 0
fi
ensure_tools
ensure_namespace
resolve_krb5_defaults
@ -1085,13 +1112,17 @@ run_test() {
run_kerberos_test_loop() {
local test_attempt=1
local test_max_attempts=2
local checker_mode_args=()
if [[ -n "$mode" ]]; then
checker_mode_args=(--mode "$mode")
fi
while true; do
if PROLE_USE_CHILD_REALM="$test_use_child" \
KRB5_REALM="$KRB5_REALM" KRB5_KDC="$effective_kdc" KRB5_ADMIN="$KRB5_ADMIN" \
KRB5_USER="${KERBEROS_TEST_ADMIN_USER:-administrator}" KRB5_PASSWORD="$KRB5_PASSWORD" \
SAMBA_ADMIN_USER="${KERBEROS_TEST_ADMIN_USER:-administrator}" SAMBA_ADMIN_PASSWORD="$KRB5_PASSWORD" \
SAMBA_DNS_SERVER="$test_samba_dns" \
"$kerberos_check_script" test; then
"$kerberos_check_script" "${checker_mode_args[@]}" test; then
return 0
fi

573
mock_val/init_knoe_auth.sh Executable file
View File

@ -0,0 +1,573 @@
#!/usr/bin/env bash
# init_knoe_auth.sh
# Provision knoe-auth (Kerberos KDC + Spring Boot enrollment service).
#
# Usage:
# ./etc/init_knoe_auth.sh [--context KUBECONTEXT] [--namespace NAMESPACE] [--project PROJECT_ID] [--mode MODE]
# ./etc/init_knoe_auth.sh initialize # full setup
# ./etc/init_knoe_auth.sh schema # schema only (idempotent)
# ./etc/init_knoe_auth.sh invite EMAIL # create first admin invite
# ./etc/init_knoe_auth.sh status # check pod + principal state
#
# Modes:
# (default / gke) GKE deploy — uses 1Password for secrets, GCP Workload Identity
# k3d Local k3d dev loop — uses hardcoded dev passwords, skips GCP steps
#
# Prerequisites:
# kubectl, op (1Password CLI — GKE mode only), psql (or kubectl exec fallback)
#
# Env vars (override args):
# APP_CLUSTER_KUBECONTEXT, KNOE_NAMESPACE, GCP_PROJECT_ID,
# KNOE_DB_HOST, KNOE_DB_PORT, KNOE_DB_NAME, KNOE_DB_SUPERUSER,
# KNOE_MODE (set to 'k3d' as alternative to --mode k3d)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
GKE_DIR="$REPO_ROOT/deploy/gcp/gke"
# ── Defaults ────────────────────────────────────────────────────────────────
MODE="${KNOE_MODE:-gke}" # gke (default) or k3d
APP_CTX="${APP_CLUSTER_KUBECONTEXT:-gke_plenary-truck-485623-p7_us-west3_knoe-dev-0}"
NAMESPACE="${KNOE_NAMESPACE:-knoe-system}"
GCP_PROJECT="${GCP_PROJECT_ID:-plenary-truck-485623-p7}"
DB_HOST="${KNOE_DB_HOST:-}" # resolved from CNPG svc if blank
DB_PORT="${KNOE_DB_PORT:-5432}"
DB_NAME="${KNOE_DB_NAME:-knoe}"
DB_SUPERUSER="${KNOE_DB_SUPERUSER:-postgres}"
REALM="KNOE.DEV"
AUTH_HOST="${KNOE_AUTH_HOST:-https://auth.knoe.dev}"
# k3d-specific defaults (overridden when --mode k3d is active)
K3D_CLUSTER_NAME="${K3D_CLUSTER_NAME:-k3d-knoe}"
K3D_CTX="k3d-${K3D_CLUSTER_NAME}"
K3D_DB_NS="${K3D_DB_NS:-knoe-db-0}"
# Dev-only passwords — NOT used in GKE mode; safe to commit
K3D_KDC_MASTER_PASSWORD="${K3D_KDC_MASTER_PASSWORD:-knoe-local-master-dev}"
K3D_KDC_ADMIN_PASSWORD="${K3D_KDC_ADMIN_PASSWORD:-knoe-local-admin-dev}"
log() { echo "[init_knoe_auth] $*"; }
info() { log "INFO $*"; }
warn() { log "WARN $*" >&2; }
die() { log "ERROR $*" >&2; exit 1; }
kube() { kubectl --context="$APP_CTX" "$@"; }
# ── Argument parsing ─────────────────────────────────────────────────────────
COMMAND="${1:-initialize}"
shift || true
while [[ $# -gt 0 ]]; do
case "$1" in
--context) APP_CTX="$2"; shift 2 ;;
--namespace) NAMESPACE="$2"; shift 2 ;;
--project) GCP_PROJECT="$2"; shift 2 ;;
--db-host) DB_HOST="$2"; shift 2 ;;
--mode) MODE="$2"; shift 2 ;;
*) break ;;
esac
done
# Apply k3d mode overrides after argument parsing
if [[ "$MODE" == "k3d" ]]; then
APP_CTX="${APP_CLUSTER_KUBECONTEXT:-${K3D_CTX}}"
REALM="KNOE.LOCAL"
AUTH_HOST="${KNOE_AUTH_HOST:-http://localhost:8080}"
# DB is accessed via port-forward (localhost:5432) in k3d mode
DB_HOST="${KNOE_DB_HOST:-localhost}"
fi
# ── Helpers ──────────────────────────────────────────────────────────────────
require_tool() {
command -v "$1" >/dev/null 2>&1 || die "Required tool not found: $1 — install it and retry."
}
wait_for_pods() {
local label="$1"
local timeout="${2:-180}"
info "Waiting up to ${timeout}s for pods with label ${label} in ${NAMESPACE}..."
kube -n "$NAMESPACE" wait pod \
-l "$label" \
--for=condition=Ready \
--timeout="${timeout}s"
}
op_secret() {
# Retrieve a 1Password secret; fall back to prompting if op isn't authed.
local item="$1" field="${2:-password}"
if command -v op >/dev/null 2>&1; then
op item get "$item" --fields "$field" 2>/dev/null || {
warn "1Password: could not read $item/$field — prompting."
read -rsp "Enter value for $item/$field: " val; echo
printf '%s' "$val"
}
else
read -rsp "Enter value for $item/$field: " val; echo
printf '%s' "$val"
fi
}
resolve_db_host() {
if [[ -n "$DB_HOST" ]]; then return; fi
if [[ "$MODE" == "k3d" ]]; then
# In k3d mode the engineer runs make k3d-knoe-pf first; DB is at localhost:5432
DB_HOST="localhost"
info "k3d mode: using DB host localhost (port-forward expected on :5432)"
return
fi
# Try to resolve CNPG primary service from the DB cluster context
DB_CTX="${DB_CLUSTER_KUBECONTEXT:-gke_plenary-truck-485623-p7_us-west3_knoe-cnpg-0}"
DB_HOST=$(kubectl --context="$DB_CTX" -n knoe-db-0 \
get svc knoe-db-rw -o jsonpath='{.spec.clusterIP}' 2>/dev/null || echo "")
[[ -z "$DB_HOST" ]] && die "Cannot resolve KNOE DB host. Set KNOE_DB_HOST or ensure knoe-db-rw svc exists."
info "Resolved DB host: $DB_HOST"
}
psql_file() {
local file="$1"
if [[ "$MODE" == "k3d" ]]; then
# In k3d mode: exec into the CNPG primary pod directly (no port-forward needed for schema)
local pod
pod=$(kubectl --context="$APP_CTX" -n "$K3D_DB_NS" \
get pod -l cnpg.io/cluster=knoe-db,role=primary \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
[[ -z "$pod" ]] && die "Cannot find CNPG primary pod in k3d. Is 'make k3d-knoe-up' complete?"
kubectl --context="$APP_CTX" -n "$K3D_DB_NS" cp "$file" "${pod}:/tmp/knoe_auth_schema.sql"
kubectl --context="$APP_CTX" -n "$K3D_DB_NS" exec "$pod" -- \
psql -U "$DB_SUPERUSER" -d "$DB_NAME" -f /tmp/knoe_auth_schema.sql
return
fi
if command -v psql >/dev/null 2>&1 && [[ -n "${PGPASSWORD:-}" ]]; then
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_SUPERUSER" -d "$DB_NAME" -f "$file"
else
# Fallback: exec into CNPG primary pod
DB_CTX="${DB_CLUSTER_KUBECONTEXT:-gke_plenary-truck-485623-p7_us-west3_knoe-cnpg-0}"
local pod
pod=$(kubectl --context="$DB_CTX" -n knoe-db-0 \
get pod -l cnpg.io/cluster=knoe-db,role=primary \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
[[ -z "$pod" ]] && die "Cannot find CNPG primary pod. Set PGPASSWORD and KNOE_DB_HOST for direct psql."
kubectl --context="$DB_CTX" -n knoe-db-0 cp "$file" "${pod}:/tmp/knoe_auth_schema.sql"
kubectl --context="$DB_CTX" -n knoe-db-0 exec "$pod" -- \
psql -U "$DB_SUPERUSER" -d "$DB_NAME" -f /tmp/knoe_auth_schema.sql
fi
}
# ── Schema ───────────────────────────────────────────────────────────────────
run_schema() {
info "Applying knoe-auth schema additions..."
resolve_db_host
local tmpfile
tmpfile=$(mktemp /tmp/knoe_auth_schema_XXXX.sql)
cat > "$tmpfile" <<'ENDSQL'
-- ── knoe-auth Round 1 schema additions ───────────────────────────────────────
-- Idempotent: all CREATE TABLE ... IF NOT EXISTS
-- Invite tokens (admin creates, single-use)
-- contact is the email/phone the invite was sent to — the OTP trust anchor.
-- knoe.dev starts with ZERO pre-knowledge of the developer's home org.
CREATE TABLE IF NOT EXISTS knoe.invitation (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
token TEXT NOT NULL UNIQUE,
contact TEXT NOT NULL,
contact_type TEXT NOT NULL DEFAULT 'email',
name_hint TEXT,
otp_hash TEXT NOT NULL,
otp_expires_at TIMESTAMPTZ NOT NULL,
otp_attempts INT NOT NULL DEFAULT 0,
otp_verified_at TIMESTAMPTZ,
created_by TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
used_by TEXT
);
CREATE INDEX IF NOT EXISTS idx_invitation_token ON knoe.invitation(token);
CREATE INDEX IF NOT EXISTS idx_invitation_contact ON knoe.invitation(contact);
-- External identity corroborations (Google sub → knoe user)
-- provider_hd records the developer's home domain (prole.org, gmail.com, etc.)
-- for audit purposes only — it is NOT used for access control.
CREATE TABLE IF NOT EXISTS knoe.identity (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES knoe.user(id) ON DELETE CASCADE,
provider TEXT NOT NULL,
provider_sub TEXT NOT NULL,
provider_email TEXT,
provider_hd TEXT,
verified_at TIMESTAMPTZ NOT NULL,
UNIQUE(provider, provider_sub)
);
CREATE INDEX IF NOT EXISTS idx_identity_user ON knoe.identity(user_id);
-- TOTP 2FA credentials (encrypted secret, backup codes)
CREATE TABLE IF NOT EXISTS knoe.totp_credential (
user_id INT PRIMARY KEY REFERENCES knoe.user(id) ON DELETE CASCADE,
secret TEXT NOT NULL,
verified_at TIMESTAMPTZ,
backup_codes TEXT[],
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Platform-managed resources (repos, db roles, policies, etc.)
CREATE TABLE IF NOT EXISTS knoe.knobject (
id SERIAL PRIMARY KEY,
type TEXT NOT NULL,
name TEXT NOT NULL,
platform_id TEXT,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(type, name)
);
-- Access grants (user → knobject with role)
CREATE TABLE IF NOT EXISTS knoe.access_grant (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES knoe.user(id),
knobject_id INT NOT NULL REFERENCES knoe.knobject(id),
role TEXT NOT NULL,
granted_by TEXT NOT NULL,
granted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
revoked_at TIMESTAMPTZ,
UNIQUE(user_id, knobject_id)
);
-- Async provisioning job queue (GitLab user, Gitea user, CNPG role, etc.)
CREATE TABLE IF NOT EXISTS knoe.provisioning_job (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES knoe.user(id),
job_type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
payload JSONB,
result JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_provisioning_job_status
ON knoe.provisioning_job(status, created_at);
-- Seed well-known knobjects
INSERT INTO knoe.knobject (type, name, metadata) VALUES
('gitea_org', 'knoey.com', '{"description": "Knoey.com Gitea organisation"}'),
('gitlab_group','knoey.com', '{"description": "Knoey.com GitLab group"}')
ON CONFLICT (type, name) DO NOTHING;
SELECT 'knoe-auth schema v1 applied.' AS status;
ENDSQL
psql_file "$tmpfile"
rm -f "$tmpfile"
info "Schema applied."
}
# ── Dev-user seed (k3d only) ─────────────────────────────────────────────────
#
# Creates the `knoe_developer` group role (the production GKE deploy uses it
# too via pg_hba.conf `+knoe_developer` rules; on GKE it was hand-rolled per
# 2026-04-30 onboarding work, never baked into postInitTemplateSQL — see
# docs/db-access.md). Then creates a `chrisfu` LOGIN role with a dev
# password and grants `knoe_developer` to it, so the engineer can connect
# from the host as chrisfu@knoey.com via the port-forward.
#
# Idempotent: re-runs on every `make k3d-knoe-up` and either creates or
# updates the role's password. This makes the rebuild loop deterministic —
# after `down && up`, chrisfu's password is always `chrisfu-dev`.
seed_dev_users_k3d() {
info "Seeding dev users (knoe_developer + chrisfu) for k3d ..."
local tmpfile
tmpfile=$(mktemp /tmp/knoe_auth_seed_XXXX.sql)
cat > "$tmpfile" <<'ENDSQL'
-- knoe_developer group role: R/W on knoe + public, R/O on auth/storage/extensions.
-- Mirrors the GKE production layout (docs/db-access.md). NOLOGIN — group only.
DO $do$ BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'knoe_developer') THEN
CREATE ROLE knoe_developer NOLOGIN;
END IF;
END $do$;
-- knoe + public — full R/W
GRANT USAGE ON SCHEMA knoe TO knoe_developer;
GRANT USAGE ON SCHEMA public TO knoe_developer;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA knoe TO knoe_developer;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO knoe_developer;
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA knoe TO knoe_developer;
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO knoe_developer;
ALTER DEFAULT PRIVILEGES IN SCHEMA knoe GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO knoe_developer;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO knoe_developer;
ALTER DEFAULT PRIVILEGES IN SCHEMA knoe GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO knoe_developer;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO knoe_developer;
-- Per-engineer LOGIN role for chrisfu (local-dev only; password is fixed dev value).
-- Creates if missing, otherwise resets the password — guarantees the rebuild loop
-- always produces the same credentials.
DO $do$ BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'chrisfu') THEN
CREATE ROLE chrisfu LOGIN INHERIT PASSWORD 'chrisfu-dev';
ELSE
ALTER ROLE chrisfu WITH LOGIN INHERIT PASSWORD 'chrisfu-dev';
END IF;
END $do$;
GRANT knoe_developer TO chrisfu;
-- Sanity check (visible in psql_file output).
SELECT 'chrisfu' AS role,
pg_has_role('chrisfu', 'knoe_developer', 'MEMBER') AS is_developer;
ENDSQL
psql_file "$tmpfile"
rm -f "$tmpfile"
info "Dev-user seed applied (chrisfu / chrisfu-dev, member of knoe_developer)."
}
# ── KDC secrets ──────────────────────────────────────────────────────────────
create_kdc_secrets() {
if kube -n "$NAMESPACE" get secret knoe-kdc-secrets >/dev/null 2>&1; then
info "knoe-kdc-secrets already exists — skipping."
return
fi
if [[ "$MODE" == "k3d" ]]; then
info "k3d mode: creating knoe-kdc-secrets with dev passwords (no 1Password)..."
kube -n "$NAMESPACE" create secret generic knoe-kdc-secrets \
--from-literal=master_password="$K3D_KDC_MASTER_PASSWORD" \
--from-literal=admin_password="$K3D_KDC_ADMIN_PASSWORD"
info "knoe-kdc-secrets created (dev passwords)."
return
fi
info "Creating knoe-kdc-secrets from 1Password..."
local master admin
master=$(op_secret "knoe-kdc-master" "password")
admin=$(op_secret "knoe-kdc-admin" "password")
kube -n "$NAMESPACE" create secret generic knoe-kdc-secrets \
--from-literal=master_password="$master" \
--from-literal=admin_password="$admin"
info "knoe-kdc-secrets created."
}
create_google_oidc_secret() {
if kube -n "$NAMESPACE" get secret knoe-auth-google-oidc >/dev/null 2>&1; then
info "knoe-auth-google-oidc already exists — skipping."
return
fi
info "Creating knoe-auth-google-oidc secret..."
local client_id client_secret
client_id=$(op_secret "knoe-google-oidc" "client_id")
client_secret=$(op_secret "knoe-google-oidc" "client_secret")
kube -n "$NAMESPACE" create secret generic knoe-auth-google-oidc \
--from-literal=client_id="$client_id" \
--from-literal=client_secret="$client_secret"
info "knoe-auth-google-oidc created."
}
create_session_secret() {
if kube -n "$NAMESPACE" get secret knoe-auth-secrets >/dev/null 2>&1; then
info "knoe-auth-secrets already exists — skipping."
return
fi
info "Creating knoe-auth-secrets (session HMAC key)..."
local session_secret
session_secret=$(op_secret "knoe-auth-session" "password")
kube -n "$NAMESPACE" create secret generic knoe-auth-secrets \
--from-literal=sessionSecret="$session_secret"
info "knoe-auth-secrets created."
}
create_oidc_path_b_secret() {
if kube -n "$NAMESPACE" get secret knoe-auth-oidc >/dev/null 2>&1; then
info "knoe-auth-oidc already exists — skipping."
return
fi
info "Creating knoe-auth-oidc secret for Path B..."
local client_id client_secret signing_key
client_id=$(op_secret "knoe-auth-oidc-gitlab" "client_id")
client_secret=$(op_secret "knoe-auth-oidc-gitlab" "client_secret")
signing_key=$(op_secret "knoe-auth-oidc-signing" "private_key")
kube -n "$NAMESPACE" create secret generic knoe-auth-oidc \
--from-literal=client-id="$client_id" \
--from-literal=client-secret="$client_secret" \
--from-literal=signing-key="$signing_key"
info "knoe-auth-oidc created."
}
# ── Manifests ────────────────────────────────────────────────────────────────
apply_manifests() {
if [[ "$MODE" == "k3d" ]]; then
local k3d_dir="$REPO_ROOT/k8s/knoe"
info "k3d mode: applying KDC manifests from $k3d_dir..."
kube apply -f "$k3d_dir/knoe-kdc-configmap.yaml"
kube apply -f "$k3d_dir/knoe-kdc-pvc.yaml"
kube apply -f "$k3d_dir/knoe-kdc-deployment.yaml"
kube apply -f "$k3d_dir/knoe-kdc-service.yaml"
info "k3d mode: skipping knoe-auth Deployment (runs on host via mvn spring-boot:run)."
return
fi
info "Applying KDC ConfigMap..."
kube apply -f "$GKE_DIR/knoe-kdc-configmap.yaml"
info "Applying knoe-auth Deployment..."
kube apply -f "$GKE_DIR/knoe-auth-deployment.yaml"
}
# ── Invite helper ─────────────────────────────────────────────────────────────
create_first_invite() {
local contact="${1:-}"
[[ -z "$contact" ]] && { read -rp "Invite contact (email or phone): " contact; }
local name_hint=""
read -rp "Display name hint (optional, press Enter to skip): " name_hint || true
info "Creating invite for: $contact"
local admin_token
admin_token=$(op_secret "knoe-admin-token" "credential" 2>/dev/null || \
{ read -rsp "Admin token: " t; echo; printf '%s' "$t"; })
local response
response=$(curl -sf -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $admin_token" \
-d "{\"contact\":\"$contact\",\"contactType\":\"email\",\"nameHint\":\"$name_hint\"}" \
"${AUTH_HOST}/auth/admin/invites") || {
warn "Admin API call failed. knoe-auth may not be ready yet."
warn "Retry: POST ${AUTH_HOST}/auth/admin/invites"
return 1
}
local invite_url
invite_url=$(printf '%s' "$response" | grep -o '"enrollUrl":"[^"]*"' | sed 's/"enrollUrl":"//;s/"//')
printf '\n\033[1;32mInvite URL:\033[0m %s\n\n' "$invite_url"
info "Send the above URL to: $contact"
}
# ── Status ────────────────────────────────────────────────────────────────────
show_status() {
if [[ "$MODE" == "k3d" ]]; then
info "=== KDC pod status ==="
kube -n "$NAMESPACE" get pods -l app=knoe-kdc 2>/dev/null || true
info "=== KDC secret ==="
kube -n "$NAMESPACE" get secret knoe-kdc-secrets 2>/dev/null || true
info "=== KDC service ==="
kube -n "$NAMESPACE" get svc knoe-kdc 2>/dev/null || true
info "=== knoe-auth runs on host ==="
info " export KRB5_CONFIG=\$PWD/etc/krb5.local.conf"
info " mvn -pl authority spring-boot:run"
return
fi
info "=== Pod status ==="
kube -n "$NAMESPACE" get pods -l app=knoe-auth 2>/dev/null || true
info "=== Secrets ==="
kube -n "$NAMESPACE" get secret \
knoe-kdc-secrets knoe-auth-google-oidc knoe-auth-secrets 2>/dev/null || true
info "=== Services ==="
kube -n "$NAMESPACE" get svc knoe-auth 2>/dev/null || true
info "=== Enrollment endpoint ==="
info "${AUTH_HOST}/auth/enroll"
}
# ── Full initialization ───────────────────────────────────────────────────────
cmd_initialize() {
require_tool kubectl
info "=== knoe-auth initialization ==="
info " Mode: $MODE"
info " Realm: $REALM"
info " Cluster: $APP_CTX"
info " NS: $NAMESPACE"
info ""
if [[ "$MODE" == "k3d" ]]; then
cmd_initialize_k3d
return
fi
# 1. Ensure namespace exists
kube get namespace "$NAMESPACE" >/dev/null 2>&1 || \
kube create namespace "$NAMESPACE"
# 2. Secrets
create_kdc_secrets
create_google_oidc_secret
create_session_secret
create_oidc_path_b_secret
# 3. Apply ConfigMap + Deployment
apply_manifests
# 4. Wait for pods
wait_for_pods "app=knoe-auth" 240
# 5. Schema
run_schema
# 6. Done
info ""
info "=== knoe-auth is ready ==="
info "Enrollment URL: ${AUTH_HOST}/auth/enroll?token=<invite_token>"
info ""
info "Next: create first admin invite:"
info " $0 invite chrisfu@knoey.com"
info ""
show_status
}
cmd_initialize_k3d() {
info "=== k3d mode: provisioning KDC + schema ==="
# 1. Ensure namespace exists
kube get namespace "$NAMESPACE" >/dev/null 2>&1 || \
kube create namespace "$NAMESPACE"
# 2. KDC secret (dev passwords, no 1Password)
create_kdc_secrets
# 3. Apply KDC manifests
apply_manifests
# 4. Wait for KDC pod
info "Waiting for KDC deployment to be ready..."
kube -n "$NAMESPACE" rollout status deployment/knoe-kdc --timeout=120s
# 5. Schema (via kubectl exec into CNPG primary)
run_schema
# 6. Dev-user seed: knoe_developer group + chrisfu role (k3d-only).
seed_dev_users_k3d
info ""
info "=== k3d knoe-auth stack is ready ==="
info "Run: make k3d-knoe-pf"
info "Then in another terminal:"
info " export KRB5_CONFIG=\$PWD/etc/krb5.local.conf"
info " export KNOE_AUTH_OIDC_SIGNING_KEY=\$(cat etc/secrets/knoe-auth-oidc-key.b64)"
info " mvn -pl authority spring-boot:run -Dspring-boot.run.profiles=k3d"
info ""
info "Dev DB access (from host, with port-forward up):"
info " PGPASSWORD=chrisfu-dev psql -h localhost -U chrisfu -d knoe-db"
info ""
show_status
}
# ── Dispatch ──────────────────────────────────────────────────────────────────
case "$COMMAND" in
initialize) cmd_initialize ;;
schema) run_schema ;;
invite) create_first_invite "${1:-}" ;;
status) show_status ;;
*)
echo "Usage: $0 {initialize|schema|invite EMAIL|status} [--context CTX] [--namespace NS]" >&2
exit 1
;;
esac

1360
mock_val/init_knoe_users.sh Executable file

File diff suppressed because it is too large Load Diff

View File

@ -22,10 +22,13 @@ _has_config=0
for _arg in "$@"; do
[[ "$_arg" == "-c" || "$_arg" == "--config" || "$_arg" == -c=* || "$_arg" == --config=* ]] && _has_config=1
done
if [[ $_has_config -eq 0 && -f "$SCRIPT_DIR/../conf/knoe.cfg" ]]; then
set -- "-c" "$SCRIPT_DIR/../conf/knoe.cfg" "$@"
if [[ $_has_config -eq 0 ]]; then
_default_cfg="$(common_core_default_config_path "$SCRIPT_DIR" || true)"
if [[ -n "$_default_cfg" ]]; then
set -- "-c" "$_default_cfg" "$@"
fi
fi
unset _has_config _arg
unset _has_config _arg _default_cfg
common_core_preparse_config "$@"
@ -39,6 +42,73 @@ if [[ -z "${KNOE_MODE:-}" ]]; then
export KNOE_MODE="k3s"
fi
resolve_explicit_kube_context() {
local ctx="${KUBECTL_CONTEXT:-${KUBE_CONTEXT_NAME:-${KUBECONTEXT:-}}}"
if [[ -n "$ctx" ]]; then
printf '%s' "$ctx"
return 0
fi
return 1
}
enforce_app_cluster_targeting() {
if [[ "${KNOE_MODE:-}" != "k8s" ]]; then
return 0
fi
local app_ctx="${APP_CLUSTER_KUBECONTEXT:-}"
local db_ctx="${DB_CLUSTER_KUBECONTEXT:-}"
local target_ctx
target_ctx="$(resolve_explicit_kube_context || true)"
if [[ -z "$app_ctx" ]]; then
echo "ERROR: APP_CLUSTER_KUBECONTEXT is required for k8s Kong deployment." >&2
exit 2
fi
if [[ -z "$target_ctx" ]]; then
echo "ERROR: explicit kubectl context is required for k8s Kong deployment." >&2
exit 2
fi
if [[ -n "$db_ctx" && "$target_ctx" == "$db_ctx" ]]; then
echo "ERROR: refusing Kong APP step against DB context '$target_ctx'." >&2
exit 2
fi
if [[ "$target_ctx" != "$app_ctx" ]]; then
echo "ERROR: Kong APP step must target APP_CLUSTER_KUBECONTEXT='$app_ctx' (got '$target_ctx')." >&2
exit 2
fi
export KUBECTL_CONTEXT="$app_ctx"
export KUBE_CONTEXT_NAME="$app_ctx"
export KUBECONTEXT="$app_ctx"
}
kubectl() {
local target_ctx
target_ctx="$(resolve_explicit_kube_context || true)"
if [[ "${KNOE_MODE:-}" == "k8s" && -z "$target_ctx" ]]; then
echo "ERROR: explicit kubectl context is required in k8s mode." >&2
return 2
fi
local arg has_context=0
for arg in "$@"; do
case "$arg" in
--context|--context=*|--server|--server=*)
has_context=1
break
;;
esac
done
if [[ -n "$target_ctx" && $has_context -eq 0 ]]; then
command kubectl --context "$target_ctx" "$@"
else
command kubectl "$@"
fi
}
enforce_app_cluster_targeting
if [[ "${COMMON_CORE_HELP:-0}" == 1 ]]; then
common_core_usage "$0"
exit 0
@ -56,16 +126,43 @@ common_core_apply_namespace "$NAMESPACE"
KNOE_HOME=${KNOE_HOME:-$(cd "$SCRIPT_DIR/.." && pwd)}
KONG_IMAGE="${KONG_IMAGE:-kong:3.9}"
KONG_NAME="${KONG_NAME:-knoe-svc-kong}"
# k8s/GKE prod mode uses knoe.dev domain and knoe-svc-kong; all other modes use knoe.org
if [[ "${KNOE_MODE:-}" == "k8s" ]]; then
KONG_NAME="${KONG_NAME:-knoe-svc-kong}"
KONG_CONFIG_NAME="${KONG_CONFIG_NAME:-knoe-svc-kong-config}"
SERVICE_HOSTNAME="${SERVICE_HOSTNAME:-svc.knoe.dev}"
AUTH_HOSTNAME="${AUTH_HOSTNAME:-api.knoe.dev}"
GITEA_HOSTNAME="${GITEA_HOSTNAME:-${GITEA_DOMAIN:-git.knoe.dev}}"
SERVICE_INGRESS_TLS_ENABLED="${SERVICE_INGRESS_TLS_ENABLED:-0}"
else
KONG_NAME="${KONG_NAME:-knoe-svc-kong}"
KONG_CONFIG_NAME="${KONG_CONFIG_NAME:-knoe-svc-kong-config}"
SERVICE_HOSTNAME="${SERVICE_HOSTNAME:-svc.prole.org}"
AUTH_HOSTNAME="${AUTH_HOSTNAME:-api.prole.org}"
GITEA_HOSTNAME="${GITEA_HOSTNAME:-${GITEA_DOMAIN:-git.prole.org}}"
DB_HOSTNAME="${DB_HOSTNAME:-db.prole.org}"
SERVICE_INGRESS_TLS_ENABLED="${SERVICE_INGRESS_TLS_ENABLED:-1}"
fi
KONG_PROXY_PORT="${KONG_PROXY_PORT:-8000}"
KONG_ADMIN_PORT="${KONG_ADMIN_PORT:-8001}"
KONG_CONFIG_NAME="${KONG_CONFIG_NAME:-knoe-svc-kong-config}"
# Public service entrypoint (single source of truth from knoe.cfg via knoe_cfg.sh)
SERVICE_HOSTNAME="${SERVICE_HOSTNAME:-svc.knoe.org}"
KONG_GITEA_SSH_PORT="${KONG_GITEA_SSH_PORT:-3022}"
SERVICE_TLS_SECRET_NAME="${SERVICE_TLS_SECRET_NAME:-${SERVICE_HOSTNAME//./-}-tls}"
SERVICE_TLS_CLUSTER_ISSUER="${SERVICE_TLS_CLUSTER_ISSUER:-letsencrypt-prod}"
# Optional: pin the GCE L7 svc-knoe-ingress to a pre-reserved GLOBAL external
# static IP (gcloud compute addresses create --global). Prevents IP churn on
# ingress delete/recreate. Leave blank to let GCE assign ephemerally.
SVC_KNOE_GLOBAL_STATIC_IP_NAME="${SVC_KNOE_GLOBAL_STATIC_IP_NAME:-}"
# BackendConfig name for the knoe-svc-kong Service. GCE's default L7 health
# check hits HTTP `/` on the backend port, which Kong responds to with 404
# (no route) -- marking the backend UNHEALTHY and causing the LB to return
# "Server Error" instead of reaching Kong. We instead point the GCE LB at a
# TCP health check (port-level liveness) so the backend passes as long as
# Kong is accepting connections, which is sufficient for our traffic shape.
# Mirrors the pattern in etc/init_gitlab.sh (gitlab-webservice-backendconfig).
SVC_KNOE_BACKEND_CONFIG_NAME="${SVC_KNOE_BACKEND_CONFIG_NAME:-knoe-svc-kong-backendconfig}"
# Legacy: svc-check used to own svc.knoe.org. We now route the service hostname
# to Grafana, so remove any leftover svc-check resources to avoid conflicts.
SVC_CHECK_NAMESPACE="${SVC_CHECK_NAMESPACE:-svc-check}"
@ -76,14 +173,27 @@ KUBECTL_APPLY_RETRIES="${KUBECTL_APPLY_RETRIES:-5}"
KUBECTL_APPLY_RETRY_DELAY="${KUBECTL_APPLY_RETRY_DELAY:-2}"
# Upstream service defaults
# oauth2-proxy for Supabase Studio (db.prole.org) — deployed by supabase/helm/oauth2-proxy
OAUTH2_PROXY_SERVICE="${OAUTH2_PROXY_SERVICE:-oauth2-proxy}"
OAUTH2_PROXY_NAMESPACE="${OAUTH2_PROXY_NAMESPACE:-supabase}"
OAUTH2_PROXY_PORT="${OAUTH2_PROXY_PORT:-80}"
DB_MANAGER_SERVICE="${DB_MANAGER_SERVICE:-knoe-db-manager}"
DB_MANAGER_PORT="${DB_MANAGER_PORT:-80}"
KNOE_SERVICE_UPSTREAM_URL="${KNOE_SERVICE_UPSTREAM_URL:-http://knoe-svc.knoe-db.svc.cluster.local:8080}"
DB_MANAGER_NAMESPACE="${DB_MANAGER_NAMESPACE:-${DATABASE_NAMESPACE:-knoe-db}}"
KNOE_SERVICE_UPSTREAM_URL="${KNOE_SERVICE_UPSTREAM_URL:-http://knoe-svc.${NAMESPACE}.svc.cluster.local:8080}"
GRAFANA_UPSTREAM_URL="${GRAFANA_UPSTREAM_URL:-http://kps-grafana.monitoring.svc.cluster.local:80}"
GITEA_HTTP_UPSTREAM_URL="${GITEA_HTTP_UPSTREAM_URL:-http://gitea-http.gitea.svc.cluster.local:3000}"
GITEA_SSH_UPSTREAM_HOST="${GITEA_SSH_UPSTREAM_HOST:-gitea-ssh.gitea.svc.cluster.local}"
GITEA_SSH_UPSTREAM_PORT="${GITEA_SSH_UPSTREAM_PORT:-22}"
# SSO wiring knobs
PROLE_GRAFANA_SSO_ENABLED="${PROLE_GRAFANA_SSO_ENABLED:-0}"
GRAFANA_PROXY_UPSTREAM_URL="${GRAFANA_PROXY_UPSTREAM_URL:-http://knoe-grafana-proxy.${NAMESPACE}.svc.cluster.local:80}"
KNOE_AUTH_UPSTREAM_URL="${KNOE_AUTH_UPSTREAM_URL:-http://knoe-auth.${SERVICE_NAMESPACE:-${NAMESPACE}}.svc.cluster.local:8080}"
usage() {
cat <<USAGE
Usage: $0 [--mode MODE] [-n NAMESPACE] [${COMMON_CORE_ACTIONS//|/|}] [-c conf/knoe.cfg]
Usage: $0 [--mode MODE] [-n NAMESPACE] [${COMMON_CORE_ACTIONS//|/|}] [-c conf/{k3d|k3s|gke}.cfg]
Actions:
start Create ConfigMap and deploy Kong
@ -101,6 +211,156 @@ ensure_tools() {
done
}
is_truthy() {
case "${1:-}" in
1|true|TRUE|True|yes|YES|on|ON|y|Y)
return 0
;;
*)
return 1
;;
esac
}
assert_public_ingress_targeting() {
local ingress_class="${1:-}"
shift || true
local hosts=("$@")
if [[ "${KNOE_MODE:-}" != "k8s" ]]; then
return 0
fi
local app_ctx="${APP_CLUSTER_KUBECONTEXT:-}"
local db_ctx="${DB_CLUSTER_KUBECONTEXT:-}"
local active_ctx="${KUBECTL_CONTEXT:-${KUBE_CONTEXT_NAME:-${KUBECONTEXT:-}}}"
if [[ -z "$app_ctx" || -z "$active_ctx" ]]; then
echo "ERROR: explicit APP cluster context is required for public Kong ingress in k8s mode." >&2
exit 1
fi
local host_count=0
local h
for h in "${hosts[@]}"; do
[[ -n "${h:-}" ]] && host_count=$((host_count + 1))
done
if [[ "$host_count" -gt 0 && -n "$db_ctx" && "$active_ctx" == "$db_ctx" ]]; then
echo "ERROR: refusing to render/apply public Kong ingress in DB cluster context '${active_ctx}' (hosts: ${hosts[*]})." >&2
exit 1
fi
if [[ "$host_count" -gt 0 && -n "$app_ctx" && -n "$active_ctx" && "$active_ctx" != "$app_ctx" ]]; then
echo "ERROR: public Kong ingress must target APP cluster context '${app_ctx}', active context is '${active_ctx}'." >&2
exit 1
fi
if [[ -n "$ingress_class" ]]; then
local normalized_class="${ingress_class,,}"
if [[ "$normalized_class" == traefik* ]] && ! is_truthy "${ALLOW_TRAEFIK_PUBLIC_INGRESS:-${KONG_ALLOW_TRAEFIK_INGRESS:-0}}"; then
echo "ERROR: ingress class '${ingress_class}' is incompatible with k8s mode unless Traefik public ingress is explicitly enabled." >&2
exit 1
fi
fi
}
_is_host_claimed_by_other_ingress() {
# Returns 0 (true) if $1 is already a host rule on any ingress other than
# $3/$2 (name/namespace). Best-effort: returns 1 on any error.
local host="${1:-}" skip_name="${2:-}" skip_ns="${3:-}"
[[ -n "$host" ]] || return 1
local ctx="${KUBECTL_CONTEXT:-${KUBE_CONTEXT_NAME:-${KUBECONTEXT:-}}}"
python3 - "$host" "$skip_ns" "$skip_name" "$ctx" <<'PY' 2>/dev/null
import json, subprocess, sys
host = sys.argv[1].strip().lower()
skip_ns, skip_name = sys.argv[2].strip(), sys.argv[3].strip()
ctx = sys.argv[4].strip()
cmd = ["kubectl"]
if ctx:
cmd += ["--context", ctx]
cmd += ["get", "ingress", "-A", "-o", "json"]
try:
raw = subprocess.check_output(cmd, text=True)
except Exception:
sys.exit(1)
for item in json.loads(raw).get("items", []):
md = item.get("metadata", {})
if (md.get("namespace") or "").strip() == skip_ns and (md.get("name") or "").strip() == skip_name:
continue
for rule in (item.get("spec", {}) or {}).get("rules", []) or []:
if (rule.get("host") or "").strip().lower() == host:
sys.exit(0)
sys.exit(1)
PY
}
assert_unique_ingress_host_claims() {
local ingress_name="${1:-}"
local ingress_namespace="${2:-}"
local host_csv="${3:-}"
[[ -n "$host_csv" ]] || return 0
local target_ctx="${KUBECTL_CONTEXT:-${KUBE_CONTEXT_NAME:-${KUBECONTEXT:-}}}"
if [[ "${KNOE_MODE:-}" == "k8s" && -z "$target_ctx" ]]; then
echo "ERROR: explicit kubectl context is required for ingress ownership checks in k8s mode." >&2
return 1
fi
if ! python3 - "$host_csv" "$ingress_namespace" "$ingress_name" "$target_ctx" <<'PY'
import json
import subprocess
import sys
requested_hosts = {h.strip().lower() for h in (sys.argv[1] or "").split(",") if h.strip()}
target_ns = sys.argv[2]
target_name = sys.argv[3]
target_ctx = (sys.argv[4] or "").strip()
cmd = ["kubectl"]
if target_ctx:
cmd.extend(["--context", target_ctx])
cmd.extend(["get", "ingress", "-A", "-o", "json"])
try:
raw = subprocess.check_output(cmd, text=True)
except Exception:
raise SystemExit(0)
if not raw.strip():
raise SystemExit(0)
payload = json.loads(raw)
conflicts: list[str] = []
for item in payload.get("items", []) or []:
md = item.get("metadata", {}) or {}
ns = (md.get("namespace") or "").strip()
name = (md.get("name") or "").strip()
if ns == target_ns and name == target_name:
continue
spec = item.get("spec", {}) or {}
rules = spec.get("rules", []) or []
for rule in rules:
host = (rule.get("host") or "").strip().lower()
if not host or host not in requested_hosts:
continue
http = rule.get("http", {}) or {}
paths = http.get("paths", []) or [{"path": "/"}]
for path_item in paths:
path = (path_item.get("path") or "/").strip() or "/"
if path in {"/", ""}:
conflicts.append(f"{host}{path} already owned by {ns}/{name}")
if conflicts:
raise SystemExit("; ".join(conflicts))
PY
then
echo "ERROR: duplicate ingress host/path claim detected for Kong ingress '${ingress_namespace}/${ingress_name}'." >&2
return 1
fi
}
ensure_namespace() {
if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then
echo "Creating namespace '$NAMESPACE' ..."
@ -139,30 +399,65 @@ _KONG_CONFIG_CHANGED_FILE=""
create_kong_config() {
echo "Creating/updating Kong declarative config '$KONG_CONFIG_NAME' in namespace '$NAMESPACE' ..."
local grafana_url
grafana_url="$GRAFANA_UPSTREAM_URL"
case "${PROLE_GRAFANA_SSO_ENABLED:-0}" in
1|true|TRUE|True|yes|YES|on|ON)
grafana_url="$GRAFANA_PROXY_UPSTREAM_URL"
;;
esac
local kong_yml
kong_yml=$(cat <<KONGEOF
_format_version: "3.0"
_transform: true
services:
# Dedicated health endpoint for the GCE LB BackendConfig. request-termination
# returns 200 synchronously with no upstream call, so the probe passes as long
# as the Kong proxy itself is running. GCE rejects \`type: TCP\` in BackendConfig
# (only HTTP/HTTPS/HTTP2 accepted), so we use this route for HTTP liveness.
#
# URL is a RFC-2606 reserved \`.invalid\` hostname that never resolves. A
# self-referential URL like \`http://127.0.0.1:\${KONG_PROXY_PORT}/\` crashlooped
# the supabase-kong pod on startup (Kong's declarative-config parser appears
# to reject the self-reference). Since request-termination short-circuits
# before any DNS lookup, a non-resolvable placeholder is equivalent.
- name: healthz
url: http://knoe.healthz.invalid/
routes:
- name: healthz
paths:
- /healthz
strip_path: true
plugins:
- name: request-termination
config:
status_code: 200
message: ok
- name: knoe-service
url: ${KNOE_SERVICE_UPSTREAM_URL}
routes:
- name: knoe-k3s-kubeconfig
hosts:
- ${SERVICE_HOSTNAME}
paths:
- /k3s/kube_config.sh
strip_path: false
- name: db-manager
url: http://${DB_MANAGER_SERVICE}.${NAMESPACE}.svc.cluster.local:${DB_MANAGER_PORT}
url: http://${DB_MANAGER_SERVICE}.${DB_MANAGER_NAMESPACE}.svc.cluster.local:${DB_MANAGER_PORT}
routes:
- name: backup-route
hosts:
- ${SERVICE_HOSTNAME}
paths:
- /backup
strip_path: false
- name: grafana
url: ${GRAFANA_UPSTREAM_URL}
url: ${grafana_url}
routes:
- name: grafana-root
hosts:
@ -170,6 +465,37 @@ services:
paths:
- /
strip_path: false
- name: knoe-auth
url: ${KNOE_AUTH_UPSTREAM_URL}
routes:
- name: knoe-auth-root
hosts:
- ${AUTH_HOSTNAME}
paths:
- /
strip_path: false
- name: gitea-http
url: ${GITEA_HTTP_UPSTREAM_URL}
routes:
- name: gitea-root
hosts:
- ${GITEA_HOSTNAME}
paths:
- /
strip_path: false
- name: gitea-ssh
host: ${GITEA_SSH_UPSTREAM_HOST}
port: ${GITEA_SSH_UPSTREAM_PORT}
protocol: tcp
routes:
- name: gitea-ssh-tcp
protocols:
- tcp
destinations:
- port: ${KONG_GITEA_SSH_PORT}
KONGEOF
)
@ -209,6 +535,27 @@ cleanup_legacy_svc_check() {
kubectl delete namespace "$SVC_CHECK_NAMESPACE" --ignore-not-found >/dev/null 2>&1 || true
}
cleanup_legacy_prole_kong() {
# Best-effort cleanup: prole-era installs deployed Kong as "prole-svc-kong".
# After the prole→knoe rebrand the canonical name is "knoe-svc-kong"; stale
# prole resources would conflict with the rollout-status wait in deploy().
local found=0
kubectl -n "$NAMESPACE" get deployment prole-svc-kong >/dev/null 2>&1 && found=1
kubectl -n "$NAMESPACE" get svc prole-svc-kong >/dev/null 2>&1 && found=1
kubectl -n "$NAMESPACE" get ingress svc-prole-ingress >/dev/null 2>&1 && found=1
if [[ "$found" -eq 0 ]]; then return 0; fi
echo "Removing legacy prole-svc-kong resources from namespace '$NAMESPACE' ..."
kubectl -n "$NAMESPACE" delete pod -l app=prole-svc-kong --ignore-not-found=true >/dev/null 2>&1 || true
kubectl -n "$NAMESPACE" delete deployment prole-svc-kong --ignore-not-found=true || true
kubectl -n "$NAMESPACE" delete svc prole-svc-kong --ignore-not-found=true || true
kubectl -n "$NAMESPACE" delete configmap prole-svc-kong-config --ignore-not-found=true || true
kubectl -n "$NAMESPACE" delete ingress svc-prole-ingress --ignore-not-found=true || true
echo "Legacy prole-svc-kong cleaned up."
# Clean up any old prole-db-* ingress resources from earlier installs.
kubectl -n "$NAMESPACE" delete ingress -l knoe.dev/route=prole-db --ignore-not-found=true >/dev/null 2>&1 || true
kubectl -n "$NAMESPACE" delete ingress prole-db-ingress --ignore-not-found=true >/dev/null 2>&1 || true
}
apply_service_ingress() {
local host="${SERVICE_HOSTNAME:-}"
if [[ -z "$host" ]]; then
@ -216,6 +563,302 @@ apply_service_ingress() {
return 0
fi
local auth_host="${AUTH_HOSTNAME:-}"
local gitea_host="${GITEA_HOSTNAME:-}"
local db_host="${DB_HOSTNAME:-}"
local include_aux_hosts=1
local include_gitea_host=1
# db.prole.org is only routed in k3s mode; oauth2-proxy handles auth before Supabase Studio.
# k8s: GKE has its own oauth2-proxy Deployment + Ingress in deploy/gcp/gke/.
# k3d: db access is port-forward only; no public hostname on the local cluster.
local include_db_host=0
local _mode="${KNOE_MODE:-}"
if [[ "$_mode" == "k3s" ]]; then
include_db_host=1
fi
if [[ "$_mode" == "k8s" || "$_mode" == "k3d" ]]; then
# k8s: GitLab/Gitea manages its own public ingress.
# k3d: git access is port-forward only; no public hostname on the local cluster.
include_gitea_host=0
fi
# Safety net: if another ingress already owns the gitea host (e.g. the git
# component deployed before Common Services), skip rather than hard-fail.
if [[ "$include_gitea_host" -eq 1 && -n "$gitea_host" ]]; then
if _is_host_claimed_by_other_ingress "$gitea_host" "svc-knoe-ingress" "$NAMESPACE"; then
echo "NOTICE: ${gitea_host} already claimed by another ingress; skipping gitea route in svc-knoe-ingress."
include_gitea_host=0
fi
fi
local tls_hosts_extra=""
local rules_extra=""
if [[ "$include_aux_hosts" -eq 1 && -n "$auth_host" && "$auth_host" != "$host" ]]; then
tls_hosts_extra=$'\n - '"${auth_host}"
rules_extra=$(cat <<EOF
- host: ${auth_host}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ${KONG_NAME}
port:
number: ${KONG_PROXY_PORT}
EOF
)
fi
if [[ "$include_gitea_host" -eq 1 && -n "$gitea_host" && "$gitea_host" != "$host" && "$gitea_host" != "$auth_host" ]]; then
tls_hosts_extra+=$'\n - '"${gitea_host}"
rules_extra+=$'\n'$(cat <<EOF
- host: ${gitea_host}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ${KONG_NAME}
port:
number: ${KONG_PROXY_PORT}
EOF
)
fi
# db.prole.org: k3s only — forward all traffic to the oauth2-proxy service
# (supabase/helm/oauth2-proxy) which gates Supabase Studio behind Google OAuth.
# TLS is terminated by Traefik upstream of Kong; /oauth2/* paths are handled
# by oauth2-proxy directly (no stripping needed — oauth2-proxy owns the path).
if [[ "$include_db_host" -eq 1 && -n "$db_host" && "$db_host" != "$host" && "$db_host" != "$auth_host" && "$db_host" != "$gitea_host" ]]; then
tls_hosts_extra+=$'\n - '"${db_host}"
rules_extra+=$'\n'$(cat <<EOF
- host: ${db_host}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ${OAUTH2_PROXY_SERVICE}
port:
number: ${OAUTH2_PROXY_PORT}
EOF
)
fi
local ingress_name="svc-knoe-ingress"
local extra_annotations=""
local tls_enabled=0
local tls_annotations=""
local gce_tls_annotations=""
local tls_block=""
local service_managed_cert_name=""
local service_frontend_config_name=""
local service_pre_shared_cert="${SERVICE_PRE_SHARED_CERT:-}"
if is_truthy "${SERVICE_INGRESS_TLS_ENABLED:-0}"; then
tls_enabled=1
fi
if [[ "${KNOE_MODE:-}" != "k8s" ]]; then
extra_annotations=$' traefik.ingress.kubernetes.io/router.priority: "10"\n'
fi
local ingress_class="${KONG_INGRESS_CLASS:-}"
if [[ -z "$ingress_class" ]]; then
if [[ "${KNOE_MODE:-}" == "k8s" ]]; then
ingress_class="gce"
else
ingress_class="traefik"
fi
fi
local ingress_hosts=("$host")
if [[ "$include_aux_hosts" -eq 1 && -n "$auth_host" && "$auth_host" != "$host" ]]; then
ingress_hosts+=("$auth_host")
fi
if [[ "$include_gitea_host" -eq 1 && -n "$gitea_host" && "$gitea_host" != "$host" && "$gitea_host" != "$auth_host" ]]; then
ingress_hosts+=("$gitea_host")
fi
if [[ "$include_db_host" -eq 1 && -n "$db_host" && "$db_host" != "$host" && "$db_host" != "$auth_host" && "$db_host" != "$gitea_host" ]]; then
ingress_hosts+=("$db_host")
fi
assert_public_ingress_targeting "$ingress_class" "${ingress_hosts[@]}"
local ingress_host_csv
ingress_host_csv=$(IFS=, ; echo "${ingress_hosts[*]}")
assert_unique_ingress_host_claims "$ingress_name" "$NAMESPACE" "$ingress_host_csv"
if [[ "${KNOE_MODE:-}" == "k8s" && "$ingress_class" == "gce" ]]; then
service_managed_cert_name="${SERVICE_MANAGED_CERT_NAME:-svc-knoe-managed-cert}"
service_frontend_config_name="${SERVICE_FRONTEND_CONFIG_NAME:-svc-knoe-frontend-config}"
local managed_domains_yaml=""
local ingress_tls_host
for ingress_tls_host in "${ingress_hosts[@]}"; do
managed_domains_yaml+=$'\n'" - ${ingress_tls_host}"
done
echo "Reconciling GKE ManagedCertificate (${service_managed_cert_name}) + FrontendConfig (${service_frontend_config_name}) for ${ingress_name} ..."
# Requirement 1: apply ManagedCertificate and FrontendConfig and confirm they exist BEFORE the ingress is applied.
kubectl apply -f - <<EOF
apiVersion: networking.gke.io/v1
kind: ManagedCertificate
metadata:
name: ${service_managed_cert_name}
namespace: ${NAMESPACE}
spec:
domains:${managed_domains_yaml}
---
apiVersion: networking.gke.io/v1beta1
kind: FrontendConfig
metadata:
name: ${service_frontend_config_name}
namespace: ${NAMESPACE}
spec:
redirectToHttps:
enabled: true
responseCodeName: MOVED_PERMANENTLY_DEFAULT
EOF
# Confirm ManagedCertificate and FrontendConfig exist before proceeding to ingress apply.
if ! kubectl -n "$NAMESPACE" get managedcertificate "$service_managed_cert_name" >/dev/null 2>&1; then
echo "ERROR: ManagedCertificate ${NAMESPACE}/${service_managed_cert_name} was not found after apply — cannot safely apply GCE ingress." >&2
return 1
fi
if ! kubectl -n "$NAMESPACE" get frontendconfig "$service_frontend_config_name" >/dev/null 2>&1; then
echo "ERROR: FrontendConfig ${NAMESPACE}/${service_frontend_config_name} was not found after apply — cannot safely apply GCE ingress." >&2
return 1
fi
echo "Confirmed: ManagedCertificate/${service_managed_cert_name} and FrontendConfig/${service_frontend_config_name} exist in ns=${NAMESPACE}."
# BackendConfig: GCE default healthCheck is HTTP GET / on the backend port
# and Kong returns 404 on an unrouted path, so the backend never goes
# HEALTHY. We wanted TCP (Kong is alive as long as it accepts connections),
# but GCE's BackendConfig CRD rejects `type: TCP` with
# `Protocol "TCP" is not valid, must be one of [HTTP,HTTPS,HTTP2]`
# so we fall back to HTTP against the `/healthz` route we add to the
# knoe-svc-kong declarative config above (request-termination plugin
# returns 200 synchronously, no upstream dependency -- equivalent liveness
# semantics to a TCP check but over a protocol GCE accepts).
echo "Reconciling BackendConfig (${SVC_KNOE_BACKEND_CONFIG_NAME}) for ${KONG_NAME} in ns=${NAMESPACE} ..."
kubectl apply -f - <<EOF
apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
name: ${SVC_KNOE_BACKEND_CONFIG_NAME}
namespace: ${NAMESPACE}
spec:
healthCheck:
type: HTTP
requestPath: /healthz
port: ${KONG_PROXY_PORT}
checkIntervalSec: 15
timeoutSec: 5
healthyThreshold: 1
unhealthyThreshold: 3
connectionDraining:
drainingTimeoutSec: 30
EOF
gce_tls_annotations=$(cat <<EOF
networking.gke.io/managed-certificates: ${service_managed_cert_name}
networking.gke.io/v1beta1.FrontendConfig: ${service_frontend_config_name}
EOF
)
# Pin to a reserved global external static IP when configured. Prevents
# IP churn on ingress delete/recreate (paired with
# conf/gke.cfg:SVC_KNOE_GLOBAL_STATIC_IP_NAME).
if [[ -n "${SVC_KNOE_GLOBAL_STATIC_IP_NAME:-}" ]]; then
gce_tls_annotations+=$'\n'" kubernetes.io/ingress.global-static-ip-name: \"${SVC_KNOE_GLOBAL_STATIC_IP_NAME}\""
fi
if [[ -n "$service_pre_shared_cert" ]]; then
echo "WARN: SERVICE_PRE_SHARED_CERT is ignored for ${NAMESPACE}/${ingress_name} in k8s/gce mode; using ManagedCertificate + FrontendConfig only." >&2
fi
unset managed_domains_yaml ingress_tls_host
fi
if (( tls_enabled == 1 )); then
tls_annotations=$(cat <<EOF
cert-manager.io/cluster-issuer: ${SERVICE_TLS_CLUSTER_ISSUER}
EOF
)
tls_block=$(cat <<EOF
tls:
- hosts:
- ${host}
${tls_hosts_extra}
secretName: ${SERVICE_TLS_SECRET_NAME}
EOF
)
else
echo "INFO: Rendering svc ingress without TLS (SERVICE_INGRESS_TLS_ENABLED=${SERVICE_INGRESS_TLS_ENABLED:-0})."
fi
# Diagnostics: show current ingress, managedcertificate, and frontendconfig state before apply.
if [[ "${KNOE_MODE:-}" == "k8s" && "$ingress_class" == "gce" ]]; then
echo "--- GCE svc ingress diagnostics (pre-apply) ---"
kubectl get ingress -A --no-headers 2>/dev/null || true
echo "ManagedCertificates:"
kubectl get managedcertificate -A --no-headers 2>/dev/null || true
echo "FrontendConfigs:"
kubectl get frontendconfig -A --no-headers 2>/dev/null || true
if kubectl -n "$NAMESPACE" get ingress "$ingress_name" >/dev/null 2>&1; then
echo "Current annotations for ${NAMESPACE}/${ingress_name} (pre-apply):"
kubectl -n "$NAMESPACE" get ingress "$ingress_name" -o jsonpath='{.metadata.annotations}' 2>/dev/null || true
echo
fi
echo "---"
fi
echo "Rendered svc ingress (pre-apply): ns=${NAMESPACE} ingress=${ingress_name} class=${ingress_class} managedCert=${service_managed_cert_name:--} frontendConfig=${service_frontend_config_name:--} preSharedCert=${service_pre_shared_cert:--} tlsEnabled=${tls_enabled} tlsSecret=${SERVICE_TLS_SECRET_NAME:--} hosts=${ingress_host_csv} backend=${KONG_NAME}:${KONG_PROXY_PORT}"
if kubectl -n "$NAMESPACE" get ingress "$ingress_name" >/dev/null 2>&1; then
local live_spec_class=""
local live_ann_class=""
local live_class=""
local live_managed_cert=""
local live_frontend_config=""
local live_pre_shared_cert=""
live_spec_class="$(kubectl -n "$NAMESPACE" get ingress "$ingress_name" -o jsonpath='{.spec.ingressClassName}' 2>/dev/null || true)"
live_ann_class="$(kubectl -n "$NAMESPACE" get ingress "$ingress_name" -o jsonpath='{.metadata.annotations.kubernetes\.io/ingress\.class}' 2>/dev/null || true)"
live_class="${live_spec_class:-$live_ann_class}"
live_managed_cert="$(kubectl -n "$NAMESPACE" get ingress "$ingress_name" -o jsonpath='{.metadata.annotations.networking\.gke\.io/managed-certificates}' 2>/dev/null || true)"
live_frontend_config="$(kubectl -n "$NAMESPACE" get ingress "$ingress_name" -o jsonpath='{.metadata.annotations.networking\.gke\.io/v1beta1\.FrontendConfig}' 2>/dev/null || true)"
live_pre_shared_cert="$(kubectl -n "$NAMESPACE" get ingress "$ingress_name" -o jsonpath='{.metadata.annotations.ingress\.gcp\.kubernetes\.io/pre-shared-cert}' 2>/dev/null || true)"
local replace_reason=""
local patch_only=0
if [[ -n "$live_class" && "$live_class" != "$ingress_class" ]]; then
# ingressClass change requires recreation (immutable field).
replace_reason="ingressClass drift (live=${live_class}, desired=${ingress_class})"
elif [[ "${KNOE_MODE:-}" == "k8s" && "$ingress_class" == "gce" ]]; then
# Requirement 2 & 3: never delete/recreate to clear stale cert annotations.
# Patch annotations in place instead.
if [[ -n "$live_pre_shared_cert" ]]; then
echo "Patching stale pre-shared-cert annotation from ${NAMESPACE}/${ingress_name} in place (was: ${live_pre_shared_cert})."
kubectl -n "$NAMESPACE" annotate ingress "$ingress_name" \
ingress.gcp.kubernetes.io/pre-shared-cert- \
"networking.gke.io/managed-certificates=${service_managed_cert_name}" \
"networking.gke.io/v1beta1.FrontendConfig=${service_frontend_config_name}" \
--overwrite >/dev/null 2>&1 || echo "WARN: Failed to patch pre-shared-cert annotation from ${ingress_name}." >&2
patch_only=1
elif [[ -n "$live_managed_cert" && "$live_managed_cert" != "$service_managed_cert_name" ]]; then
echo "Patching managed certificate annotation on ${NAMESPACE}/${ingress_name} in place (live=${live_managed_cert}, desired=${service_managed_cert_name})."
kubectl -n "$NAMESPACE" annotate ingress "$ingress_name" \
"networking.gke.io/managed-certificates=${service_managed_cert_name}" \
--overwrite >/dev/null 2>&1 || echo "WARN: Failed to patch managed-certificates annotation on ${ingress_name}." >&2
patch_only=1
elif [[ -n "$live_frontend_config" && "$live_frontend_config" != "$service_frontend_config_name" ]]; then
echo "Patching frontend config annotation on ${NAMESPACE}/${ingress_name} in place (live=${live_frontend_config}, desired=${service_frontend_config_name})."
kubectl -n "$NAMESPACE" annotate ingress "$ingress_name" \
"networking.gke.io/v1beta1.FrontendConfig=${service_frontend_config_name}" \
--overwrite >/dev/null 2>&1 || echo "WARN: Failed to patch FrontendConfig annotation on ${ingress_name}." >&2
patch_only=1
fi
fi
if [[ -n "$replace_reason" && "$patch_only" -eq 0 ]]; then
echo "Ingress shape change detected for ${NAMESPACE}/${ingress_name}: ${replace_reason}. Replacing ingress (host/rule shape change)."
kubectl -n "$NAMESPACE" delete ingress "$ingress_name" --ignore-not-found >/dev/null || true
fi
unset patch_only
fi
echo "Applying service Ingress for host '${host}' -> ${KONG_NAME}:${KONG_PROXY_PORT} (namespace=${NAMESPACE}) ..."
(
tmp="$(mktemp)"
@ -224,17 +867,13 @@ apply_service_ingress() {
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: svc-knoe-ingress
name: ${ingress_name}
namespace: ${NAMESPACE}
annotations:
kubernetes.io/ingress.class: traefik
traefik.ingress.kubernetes.io/router.priority: "10"
cert-manager.io/cluster-issuer: ${SERVICE_TLS_CLUSTER_ISSUER}
kubernetes.io/ingress.class: ${ingress_class}
${extra_annotations}${tls_annotations}${gce_tls_annotations}
spec:
tls:
- hosts:
- ${host}
secretName: ${SERVICE_TLS_SECRET_NAME}
${tls_block}
rules:
- host: ${host}
http:
@ -246,15 +885,38 @@ spec:
name: ${KONG_NAME}
port:
number: ${KONG_PROXY_PORT}
${rules_extra}
EOF
kubectl_apply_retry -f "$tmp"
)
if [[ "${KNOE_MODE:-}" == "k8s" && "$ingress_class" == "gce" ]]; then
kubectl -n "$NAMESPACE" annotate ingress "$ingress_name" \
ingress.gcp.kubernetes.io/pre-shared-cert- \
--overwrite >/dev/null 2>&1 || true
# Diagnostics: show annotations after apply.
echo "Annotations for ${NAMESPACE}/${ingress_name} (post-apply):"
kubectl -n "$NAMESPACE" get ingress "$ingress_name" -o jsonpath='{.metadata.annotations}' 2>/dev/null || true
echo
kubectl -n "$NAMESPACE" describe ingress "$ingress_name" 2>/dev/null || true
else
kubectl -n "$NAMESPACE" annotate ingress "$ingress_name" \
networking.gke.io/managed-certificates- \
networking.gke.io/v1beta1.FrontendConfig- \
ingress.gcp.kubernetes.io/pre-shared-cert- \
--overwrite >/dev/null 2>&1 || true
fi
}
deploy() {
echo "Deploying $KONG_NAME to namespace '$NAMESPACE' ..."
local manifests_dir="$KNOE_HOME/deploy/opentofu/k3s/manifests/knoe"
local manifests_dir
if [[ "${KNOE_MODE:-}" == "k8s" ]]; then
manifests_dir="$KNOE_HOME/deploy/opentofu/k8s/manifests/knoe"
else
manifests_dir="$KNOE_HOME/deploy/opentofu/k3s/manifests/knoe"
fi
# Determine whether the Deployment already exists before applying manifests.
local deployment_existed=0
@ -267,6 +929,18 @@ deploy() {
svc_out=$(kubectl_apply_retry -f "$manifests_dir/kong-service.yaml" -n "$NAMESPACE" 2>&1)
echo "$svc_out"
# In k8s/GCE mode, annotate the Service so GCE LB picks up the BackendConfig
# with the TCP health check. Matches etc/init_gitlab.sh's pattern for
# gitlab-webservice-default. Additive annotation; survives manifest
# re-applies (the YAML in deploy/opentofu/ doesn't set it).
if [[ "${KNOE_MODE:-}" == "k8s" ]]; then
echo "Annotating Service ${KONG_NAME} with cloud.google.com/backend-config=${SVC_KNOE_BACKEND_CONFIG_NAME}..."
kubectl -n "$NAMESPACE" annotate svc "$KONG_NAME" \
"cloud.google.com/backend-config={\"default\":\"${SVC_KNOE_BACKEND_CONFIG_NAME}\"}" \
--overwrite >/dev/null || \
echo "WARN: Failed to annotate ${KONG_NAME} with backend-config; GCE LB will fall back to default healthcheck (likely UNHEALTHY)." >&2
fi
echo "Waiting for $KONG_NAME rollout ..."
kubectl rollout status deployment/"$KONG_NAME" -n "$NAMESPACE" --timeout=120s
@ -303,6 +977,14 @@ status() {
echo ""
echo "=== $KONG_NAME service ==="
kubectl get svc "$KONG_NAME" -n "$NAMESPACE" 2>/dev/null || echo "No service found"
echo ""
echo "=== svc-knoe-ingress hosts (includes db.prole.org in k3s mode) ==="
kubectl get ingress svc-knoe-ingress -n "$NAMESPACE" -o jsonpath='{range .spec.rules[*]}{.host}{"\n"}{end}' 2>/dev/null || echo "No ingress found"
if [[ "${KNOE_MODE:-}" == "k3s" ]]; then
echo ""
echo "=== oauth2-proxy (db.prole.org gate, ns=${OAUTH2_PROXY_NAMESPACE}) ==="
kubectl get pods -n "${OAUTH2_PROXY_NAMESPACE}" -l app.kubernetes.io/name=oauth2-proxy 2>/dev/null || echo "No oauth2-proxy pods found"
fi
}
restart() {
@ -316,6 +998,7 @@ action_update() {
ensure_tools
ensure_namespace
cleanup_legacy_svc_check
cleanup_legacy_prole_kong
create_kong_config
apply_service_ingress
deploy

52
mock_val/init_min.sh Normal file
View File

@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
# etc/init_min.sh
# Purpose: Initialize minimal containerd environment for knoe-db development
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# shellcheck disable=SC1090
source "$SCRIPT_DIR/knoe_cfg.sh"
log() { echo "==> $*"; }
ACTION=${1:-initialize}
case "$ACTION" in
initialize)
log "Initializing minimal environment..."
# Ensure containerd is running (if on macOS via brew)
if [[ "$OSTYPE" == "darwin"* ]]; then
if ! pgrep containerd >/dev/null; then
log "Starting containerd via brew services..."
brew services start containerd
sleep 2
fi
fi
# Create start.sh and stop.sh aliases to knoe.sh
log "Generating start.sh and stop.sh as wrappers to knoe.sh..."
cat > "$PROJECT_ROOT/start.sh" <<EOF
#!/usr/bin/env bash
exec "\$(dirname "\$0")/knoe.sh" --min start "\$@"
EOF
chmod +x "$PROJECT_ROOT/start.sh"
cat > "$PROJECT_ROOT/stop.sh" <<EOF
#!/usr/bin/env bash
exec "\$(dirname "\$0")/knoe.sh" --min stop "\$@"
EOF
chmod +x "$PROJECT_ROOT/stop.sh"
log "Minimal environment initialized successfully."
;;
*)
log "Unknown action: $ACTION"
exit 1
;;
esac

View File

@ -86,30 +86,56 @@ default_storage_class() {
kubectl get storageclass -o jsonpath='{range .items[?(@.metadata.annotations.storageclass\.kubernetes\.io/is-default-class=="true")]}{.metadata.name}{"\n"}{end}' 2>/dev/null | head -n1
}
monitoring_mode() {
local mode
mode="${KNOE_MODE:-${DEPLOYMENT_MODE:-}}"
mode=$(printf '%s' "$mode" | tr '[:upper:]' '[:lower:]')
case "$mode" in
gke|prod|k8s)
echo "k8s"
;;
k3s|k3d)
echo "$mode"
;;
*)
echo "$mode"
;;
esac
}
choose_monitoring_storage_class() {
if [[ -n "${MONITORING_STORAGE_CLASS:-}" ]] && storage_class_exists "$MONITORING_STORAGE_CLASS"; then
local mode
mode=$(monitoring_mode)
if [[ -n "${MONITORING_STORAGE_CLASS:-}" ]]; then
echo "$MONITORING_STORAGE_CLASS"
return 0
fi
if storage_class_exists "merlin-local-iscsi"; then
echo "merlin-local-iscsi"
return 0
fi
# Backward compatibility (older runs created knoe-monitoring-<volumeId>)
if storage_class_exists "knoe-monitoring-d004"; then
echo "knoe-monitoring-d004"
return 0
if [[ "$mode" == "k3s" ]]; then
if storage_class_exists "merlin-local-iscsi"; then
echo "merlin-local-iscsi"
return 0
fi
# Backward compatibility (older runs created knoe-monitoring-<volumeId>)
if storage_class_exists "knoe-monitoring-d004"; then
echo "knoe-monitoring-d004"
return 0
fi
fi
local default_sc
default_sc=$(default_storage_class)
if [[ -n "$default_sc" ]]; then
echo "$default_sc"
return 0
fi
if storage_class_exists "local-path"; then
if [[ "$mode" == "k3s" || "$mode" == "k3d" ]] && storage_class_exists "local-path"; then
echo "local-path"
return 0
fi
echo ""
}
@ -120,7 +146,7 @@ monitoring_storage_class_for_role() {
# For the static local-PV setup on the primary k3s node, use dedicated
# storageClasses per component to avoid nondeterministic PV binding.
if [[ "$base" == "merlin-local-iscsi" ]]; then
if [[ "$(monitoring_mode)" == "k3s" && "$base" == "merlin-local-iscsi" ]]; then
local candidate
candidate="${base}-${role}"
if storage_class_exists "$candidate"; then
@ -134,8 +160,45 @@ monitoring_storage_class_for_role() {
echo "$base"
}
validate_monitoring_storage_classes() {
local selected="${MONITORING_STORAGE_CLASS_SELECTED:-}"
[[ -n "${selected:-}" ]] || return 0
local role sc
local -a missing=()
local -a seen=()
for role in prometheus alertmanager grafana; do
sc=$(monitoring_storage_class_for_role "$role")
[[ -n "${sc:-}" ]] || continue
local already_seen=0
local seen_sc
for seen_sc in "${seen[@]}"; do
if [[ "$seen_sc" == "$sc" ]]; then
already_seen=1
break
fi
done
(( already_seen == 1 )) && continue
seen+=("$sc")
if ! storage_class_exists "$sc"; then
missing+=("$sc")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
local mode
mode=$(monitoring_mode)
err "Monitoring storageClass validation failed for mode '${mode:-unknown}': rendered class(es) not found: ${missing[*]}. Configure MONITORING_STORAGE_CLASS in conf/gke.cfg (or the selected cluster config) to an existing StorageClass."
return 1
fi
return 0
}
monitoring_nodes_available() {
kubectl get nodes -l "knoe.org/node-role=general" -o name 2>/dev/null | grep -q .
kubectl get nodes -l "prole.org/node-role=general" -o name 2>/dev/null | grep -q .
}
resolve_monitoring_primary_node() {
@ -144,11 +207,11 @@ resolve_monitoring_primary_node() {
printf '%s' "${MONITORING_PRIMARY_NODE}"
return 0
fi
if kubectl get node merlin.knoe.org >/dev/null 2>&1; then
printf '%s' "merlin.knoe.org"
if kubectl get node merlin.prole.org >/dev/null 2>&1; then
printf '%s' "merlin.prole.org"
return 0
fi
node=$(kubectl get nodes -l "knoe.org/node-role=general" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
node=$(kubectl get nodes -l "prole.org/node-role=general" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -n "${node:-}" ]]; then
printf '%s' "$node"
return 0
@ -161,7 +224,7 @@ prepare_k3s_monitoring_data_dirs() {
local node="$1"
# NOTE: Historically this came from knoe.cfg. For now we pin it to the
# monitoring deployment default and allow overrides via env var.
local base="${PROLE_MONITORING_DATA_DIR:-/knoe/d004}"
local base="${PROLE_MONITORING_DATA_DIR:-/synology/d004}"
base="${base%/}"
if [[ -z "${node:-}" ]]; then
err "Could not resolve a monitoring node to prepare storage on"
@ -256,7 +319,7 @@ apply_k3s_monitoring_local_pvs() {
fi
local base volume_id sc_base sc_prom sc_am sc_graf
base="${PROLE_MONITORING_DATA_DIR:-/knoe/d004}"
base="${PROLE_MONITORING_DATA_DIR:-/synology/d004}"
base="${base%/}"
volume_id=$(basename "$base")
if [[ -z "${volume_id:-}" || "$volume_id" == "/" || "$volume_id" == "." ]]; then
@ -398,7 +461,7 @@ render_node_selector() {
local indent="$1"
cat <<EOF
${indent}nodeSelector:
${indent} knoe.org/node-role: general
${indent} prole.org/node-role: general
EOF
}
@ -429,7 +492,7 @@ render_tolerations() {
mode="${KNOE_MODE:-${DEPLOYMENT_MODE:-}}"
local key value effect
key="${MONITORING_TAINT_KEY:-knoe.org/monitoring}"
key="${MONITORING_TAINT_KEY:-prole.org/monitoring}"
value="${MONITORING_TAINT_VALUE:-true}"
effect="${MONITORING_TAINT_EFFECT:-NoSchedule}"
@ -452,7 +515,7 @@ ${indent} nodeAffinity:
${indent} requiredDuringSchedulingIgnoredDuringExecution:
${indent} nodeSelectorTerms:
${indent} - matchExpressions:
${indent} - key: knoe.org/node-role
${indent} - key: prole.org/node-role
${indent} operator: In
${indent} values:
${indent} - general
@ -552,12 +615,51 @@ render_grafana_external_url() {
if [[ -z "$host" ]]; then
return 0
fi
local sso_enabled="${PROLE_GRAFANA_SSO_ENABLED:-0}"
local sso_block=""
case "$sso_enabled" in
1|true|TRUE|True|yes|YES|on|ON)
sso_block=$(cat <<EOF
${indent} auth.proxy:
${indent} enabled: true
${indent} header_name: X-WEBAUTH-USER
${indent} header_property: username
${indent} auto_sign_up: true
${indent} auth:
${indent} disable_login_form: true
${indent} auth.anonymous:
${indent} enabled: false
EOF
)
;;
esac
local google_client_id="${GRAFANA_GOOGLE_CLIENT_ID:-}"
local google_client_secret="${GRAFANA_GOOGLE_CLIENT_SECRET:-}"
local google_block=""
if [[ -n "$google_client_id" && -n "$google_client_secret" ]]; then
google_block=$(cat <<EOF
${indent} auth.google:
${indent} enabled: true
${indent} client_id: "${google_client_id}"
${indent} client_secret: "${google_client_secret}"
${indent} scopes: openid email profile
${indent} auth_url: https://accounts.google.com/o/oauth2/v2/auth
${indent} token_url: https://accounts.google.com/o/oauth2/token
${indent} api_url: https://www.googleapis.com/oauth2/v3/userinfo
${indent} allowed_domains: knoey.com
${indent} use_pkce: true
EOF
)
fi
cat <<EOF
${indent}grafana.ini:
${indent} server:
${indent} domain: "${host}"
${indent} root_url: "https://${host}/"
${indent} serve_from_sub_path: false
${sso_block}${google_block}
EOF
}
@ -599,7 +701,18 @@ apply_grafana_dashboard() {
local tmp
tmp=$(mktemp)
sed "s/\\\${DS_PROMETHEUS}/${prom_uid}/g" "$dashboard_path" > "$tmp"
# Pass the dashboard JSON through unchanged. The source JSON templates the
# datasource UID via the dashboard's own `DS_PROMETHEUS` variable (resolved
# at render time by Grafana, configurable per-user via the Datasource
# dropdown). The earlier sed-substitution to a fixed `prom_uid` baked
# `uid: prometheus` into every variable + panel definition, defeating that
# template — the dashboard then queried only the default Prometheus
# datasource regardless of what the user selected. With cross-cluster
# CNPG metrics on `cnpg-prometheus` (commit 09f2c1a), passing through
# unchanged is required for variable dropdowns + panels to follow the
# Datasource selector.
cp "$dashboard_path" "$tmp"
: "$prom_uid" # silence unused-var warning
local cm_yaml
cm_yaml=$(mktemp)
@ -613,35 +726,19 @@ apply_grafana_dashboard() {
}
apply_grafana_datasource() {
local ns="$1"
local prom_uid="${GRAFANA_PROMETHEUS_DATASOURCE_UID:-prometheus}"
local prom_url="${GRAFANA_PROMETHEUS_DATASOURCE_URL:-http://kps-kube-prometheus-stack-prometheus.${ns}.svc.cluster.local:9090}"
local cm_yaml
cm_yaml=$(mktemp)
cat > "$cm_yaml" <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: knoe-grafana-datasource
namespace: ${ns}
labels:
grafana_datasource: "1"
data:
knoe-prometheus-datasource.yaml: |
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
uid: ${prom_uid}
access: proxy
url: ${prom_url}
isDefault: true
editable: false
EOF
kubectl_retry -n "$ns" apply --server-side --field-manager=knoe-init-monitoring --force-conflicts -f "$cm_yaml" >/dev/null
rm -f "$cm_yaml"
# No-op: kube-prometheus-stack already provisions the Prometheus + Alertmanager
# datasources via its own ConfigMap (`<release>-grafana-datasource`). The
# earlier extra `knoe-grafana-datasource` ConfigMap created here was a
# duplicate of that — it registered the same `uid: prometheus`, which collided
# with the chart's datasource and caused Grafana's provisioning reload to
# error out (HTTP 500 → datasources never refreshed).
#
# Additional datasources (e.g. the cross-cluster `cnpg-prometheus` pointing
# at the DB-cluster Prometheus) are wired through the chart values at
# `monitoring/kps-values-gke.yaml` `grafana.additionalDataSources`, NOT
# through this script. Keeping this function as a no-op so call sites don't
# need to change.
return 0
}
helm_release_status() {
@ -1059,6 +1156,10 @@ install_monitoring() {
log "No storageClass detected; disabling persistence for Grafana and skipping Prometheus/Alertmanager storage."
fi
if ! validate_monitoring_storage_classes; then
return 1
fi
# Dont enforce a single storageClass here: in k3s static local-PV mode we use
# per-component storageClasses (e.g. merlin-local-iscsi-prometheus|grafana|alertmanager).
cleanup_pending_pvcs "$monitoring_ns" ""
@ -1074,6 +1175,23 @@ install_monitoring() {
;;
esac
# Derive PV names from PROLE_MONITORING_DATA_DIR (same logic as apply_monitoring_pvs)
local _mpv_base _mpv_vid
_mpv_base="${PROLE_MONITORING_DATA_DIR:-/synology/d004}"
_mpv_base="${_mpv_base%/}"
_mpv_vid=$(basename "$_mpv_base")
pv_prom="merlin-local-iscsi-${_mpv_vid}-prometheus"
pv_am="merlin-local-iscsi-${_mpv_vid}-alertmanager"
pv_graf="merlin-local-iscsi-${_mpv_vid}-grafana"
# Release any Released monitoring PVs so new PVCs can bind (idempotent)
for _mpv in "${pv_prom}" "${pv_am}" "${pv_graf}"; do
_mpv_phase=$(kubectl get pv "$_mpv" -o jsonpath='{.status.phase}' 2>/dev/null || true)
if [[ "$_mpv_phase" == "Released" ]]; then
log "Clearing stale claimRef on Released PV '$_mpv' ..."
kubectl patch pv "$_mpv" -p '{"spec":{"claimRef":null}}' 2>/dev/null || true
fi
done
section "Installing Prometheus stack"
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts || true
helm repo update prometheus-community || true
@ -1181,6 +1299,15 @@ prometheus-node-exporter:
$(render_node_selector " ")
$(render_node_affinity " ")
$(render_tolerations " ")
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: NotIn
values:
- pi.prole.org
EOF
local monitoring_helm_server_side
@ -1278,6 +1405,8 @@ fullnameOverride: kps-grafana
adminPassword: "${GRAFANA_ADMIN_PASSWORD}"
$(render_grafana_external_url "")
service:
port: 80
targetPort: 3000

View File

@ -61,7 +61,7 @@ if [[ -n "$MODE" && "$MODE" != "k3d" ]]; then
fi
knoe_ensure_kubeconfig >/dev/null 2>&1 || true
knoe_ensure_kube_context || exit 1
ensure_kube_context || exit 1
PORT_MAPPING_FILE_PATH="${PORT_MAPPING_FILE:-}"
if [[ -z "$PORT_MAPPING_FILE_PATH" ]]; then

127
mock_val/init_oauth2_proxy.sh Executable file
View File

@ -0,0 +1,127 @@
#!/usr/bin/env bash
# init_oauth2_proxy.sh
#
# Bootstrap the oauth2-proxy gate in front of Supabase Studio at
# db.0.knoe.dev. Gates access via Google Workspace OIDC (knoey.com) so any
# @knoey.com identity (chrisfu, ron) can sign in and share the Studio
# session. Outside-domain users are rejected at this layer.
#
# Usage:
# ./etc/init_oauth2_proxy.sh
#
# Env vars (resolved from etc/secrets/* if not set in the shell):
# OAUTH2_PROXY_CLIENT_ID ← from etc/secrets/oauth2-proxy-client-id
# OAUTH2_PROXY_CLIENT_SECRET ← from etc/secrets/oauth2-proxy-client-secret
# OAUTH2_PROXY_COOKIE_SECRET ← from etc/secrets/oauth2-proxy-cookie-secret
#
# Optional:
# APP_CLUSTER_KUBECONTEXT (default: $KUBECONTEXT then ambient)
# NAMESPACE (default: supabase)
#
# Pre-reqs:
# - OAuth 2.0 client created at GCP Console (see the secret template
# deploy/gcp/gke/oauth2-proxy-google-oidc-secret.example.yaml for the
# exact authorized redirect URI + consent screen settings).
# - cookie_secret generated with: openssl rand -base64 32
# - Three values saved into etc/secrets/oauth2-proxy-{client-id,client-secret,cookie-secret}
# (chmod 0600 each; etc/secrets/ is gitignored except for .keep).
#
# After this script runs and the oauth2-proxy Deployment is Ready, two
# manual steps complete the wiring (NOT done by this script — see the plan
# in docs/plans/ for the full sequence):
#
# 1. Patch the supabase-kong Ingress to route db.0.knoe.dev through
# oauth2-proxy:80 instead of supabase-kong:8000:
#
# kubectl --context=$APP_CLUSTER_KUBECONTEXT -n supabase patch ingress \
# supabase-kong --type=json -p '[
# {"op": "replace",
# "path": "/spec/rules/1/http/paths/0/backend/service/name",
# "value": "oauth2-proxy"},
# {"op": "replace",
# "path": "/spec/rules/1/http/paths/0/backend/service/port/number",
# "value": 80}
# ]'
# (verify the index by checking which rule has host=db.0.knoe.dev first;
# index may shift on future Helm reconciles)
#
# 2. Remove the basic-auth plugin from the dashboard route in the
# supabase-kong configmap (oauth2-proxy is the gate now; double-auth is
# friction). Then rollout-restart supabase-kong.
#
# When knoe-auth Round 1 ships an OIDC OP at https://api.knoe.dev/auth, change
# `--provider=google` to `--provider=oidc --oidc-issuer-url=https://api.knoe.dev/auth`
# in deploy/gcp/gke/oauth2-proxy-deployment.yaml and re-run this script.
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"
NAMESPACE="${NAMESPACE:-supabase}"
KCTX="${APP_CLUSTER_KUBECONTEXT:-${KUBECONTEXT:-}}"
if [[ -n "$KCTX" ]]; then
KCTX_FLAG=(--context="$KCTX")
else
KCTX_FLAG=()
fi
log() { printf "[%s] %s\n" "$(date +%H:%M:%S)" "$*"; }
die() { log "ERROR: $*" >&2; exit 1; }
resolve_secret() {
# Resolve a value from env (preferred) or etc/secrets/<file>.
local var="$1" file="$2" val="${!1:-}"
if [[ -z "$val" && -f "$REPO_ROOT/etc/secrets/$file" ]]; then
val="$(cat "$REPO_ROOT/etc/secrets/$file")"
fi
if [[ -z "$val" ]]; then
die "missing $var (set the env var, or save the value into etc/secrets/$file)"
fi
printf '%s' "$val"
}
for tool in kubectl envsubst; do
command -v "$tool" >/dev/null 2>&1 || die "required tool not found: $tool"
done
OAUTH2_PROXY_CLIENT_ID="$(resolve_secret OAUTH2_PROXY_CLIENT_ID oauth2-proxy-client-id)"
OAUTH2_PROXY_CLIENT_SECRET="$(resolve_secret OAUTH2_PROXY_CLIENT_SECRET oauth2-proxy-client-secret)"
OAUTH2_PROXY_COOKIE_SECRET="$(resolve_secret OAUTH2_PROXY_COOKIE_SECRET oauth2-proxy-cookie-secret)"
export OAUTH2_PROXY_CLIENT_ID OAUTH2_PROXY_CLIENT_SECRET OAUTH2_PROXY_COOKIE_SECRET
SECRET_TMPL="$GKE_MANIFEST_DIR/oauth2-proxy-google-oidc-secret.example.yaml"
DEPLOY_MANIFEST="$GKE_MANIFEST_DIR/oauth2-proxy-deployment.yaml"
[[ -f "$SECRET_TMPL" ]] || die "missing manifest: $SECRET_TMPL"
[[ -f "$DEPLOY_MANIFEST" ]] || die "missing manifest: $DEPLOY_MANIFEST"
log "==> oauth2-proxy bootstrap"
log " namespace : $NAMESPACE"
log " kubectx : ${KCTX:-<ambient>}"
log "Applying oauth2-proxy-google-oidc secret ..."
envsubst '${OAUTH2_PROXY_CLIENT_ID} ${OAUTH2_PROXY_CLIENT_SECRET} ${OAUTH2_PROXY_COOKIE_SECRET}' \
< "$SECRET_TMPL" \
| kubectl "${KCTX_FLAG[@]}" -n "$NAMESPACE" apply -f -
log "Applying oauth2-proxy ServiceAccount + BackendConfig + Service + Deployment ..."
kubectl "${KCTX_FLAG[@]}" -n "$NAMESPACE" apply -f "$DEPLOY_MANIFEST"
log "Waiting for oauth2-proxy Deployment to become Ready (timeout 180s) ..."
kubectl "${KCTX_FLAG[@]}" -n "$NAMESPACE" rollout status deployment/oauth2-proxy --timeout=180s
log "==> oauth2-proxy bootstrap complete."
echo ""
echo " Next steps (NOT performed by this script):"
echo " 1. Patch the supabase-kong Ingress so db.0.knoe.dev routes to"
echo " oauth2-proxy:80 instead of supabase-kong:8000."
echo " 2. Remove the basic-auth plugin from the dashboard route in the"
echo " supabase-kong configmap, then rollout-restart supabase-kong."
echo " 3. In a browser, sign in to https://db.0.knoe.dev/ with a"
echo " @knoey.com Google account. Try a non-knoey account too — should"
echo " receive 403 from oauth2-proxy."
echo ""
echo " See the active plan in ~/.claude/plans/ for the exact patch commands."

View File

@ -0,0 +1,116 @@
#!/usr/bin/env bash
# init_oauth2_proxy_prole.sh
#
# Bootstrap the oauth2-proxy gate in front of Supabase Studio at db.prole.org
# on the k3s homelab cluster. Companion to init_oauth2_proxy.sh (knoe.dev GKE)
# but uses prole.org GCP project credentials and targets the k3s kubecontext.
#
# Gates access via Google Workspace OIDC (prole.org) so @prole.org identities
# can sign in to Studio. Outside-domain users are rejected at this layer.
#
# Usage:
# ./etc/init_oauth2_proxy_prole.sh
#
# Env vars (resolved from etc/secrets/* if not set in the shell):
# OAUTH2_PROXY_CLIENT_ID ← from etc/secrets/oauth2-proxy-client-id-prole
# OAUTH2_PROXY_CLIENT_SECRET ← from etc/secrets/oauth2-proxy-client-secret-prole
# OAUTH2_PROXY_COOKIE_SECRET ← from etc/secrets/oauth2-proxy-cookie-secret-prole
#
# Optional:
# K3S_KUBECONTEXT (default: $KUBECONTEXT then ambient)
# NAMESPACE (default: supabase)
#
# Pre-reqs:
# - OAuth 2.0 Web Application client created in the prole.org GCP project:
# Authorized JS origins: https://db.prole.org
# Authorized redirect URI: https://db.prole.org/oauth2/callback
# Consent screen: Internal (prole.org Workspace)
# Scopes: openid, email, profile
# See deploy/gcp/gke/oauth2-proxy-google-oidc-secret-prole.example.yaml.
# - Cookie secret generated with: openssl rand -base64 32
# - Three values saved (chmod 0600) to:
# etc/secrets/oauth2-proxy-client-id-prole
# etc/secrets/oauth2-proxy-client-secret-prole
# etc/secrets/oauth2-proxy-cookie-secret-prole
#
# After this script runs, patch the db.prole.org Ingress/IngressRoute to route
# through oauth2-proxy:80 instead of supabase-kong:8000 directly (see next
# steps printed at the end of this script).
#
# When knoe-auth Round 1 ships an OIDC OP at https://api.prole.org/auth, change
# --provider=google to --provider=oidc --oidc-issuer-url=https://api.prole.org/auth
# in deploy/opentofu/k3s/manifests/knoe/oauth2-proxy-deployment-prole.yaml and
# re-run this script.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
MANIFEST_DIR="$REPO_ROOT/deploy/gcp/gke"
K3S_MANIFEST_DIR="$REPO_ROOT/deploy/opentofu/k3s/manifests/knoe"
NAMESPACE="${NAMESPACE:-supabase}"
KCTX="${K3S_KUBECONTEXT:-${KUBECONTEXT:-}}"
if [[ -n "$KCTX" ]]; then
KCTX_FLAG=(--context="$KCTX")
else
KCTX_FLAG=()
fi
log() { printf "[%s] %s\n" "$(date +%H:%M:%S)" "$*"; }
die() { log "ERROR: $*" >&2; exit 1; }
resolve_secret() {
local var="$1" file="$2" val="${!1:-}"
if [[ -z "$val" && -f "$REPO_ROOT/etc/secrets/$file" ]]; then
val="$(cat "$REPO_ROOT/etc/secrets/$file")"
fi
if [[ -z "$val" ]]; then
die "missing $var (set the env var, or save the value into etc/secrets/$file)"
fi
printf '%s' "$val"
}
for tool in kubectl envsubst; do
command -v "$tool" >/dev/null 2>&1 || die "required tool not found: $tool"
done
OAUTH2_PROXY_CLIENT_ID="$(resolve_secret OAUTH2_PROXY_CLIENT_ID oauth2-proxy-client-id-prole)"
OAUTH2_PROXY_CLIENT_SECRET="$(resolve_secret OAUTH2_PROXY_CLIENT_SECRET oauth2-proxy-client-secret-prole)"
OAUTH2_PROXY_COOKIE_SECRET="$(resolve_secret OAUTH2_PROXY_COOKIE_SECRET oauth2-proxy-cookie-secret-prole)"
export OAUTH2_PROXY_CLIENT_ID OAUTH2_PROXY_CLIENT_SECRET OAUTH2_PROXY_COOKIE_SECRET
SECRET_TMPL="$MANIFEST_DIR/oauth2-proxy-google-oidc-secret-prole.example.yaml"
DEPLOY_MANIFEST="$K3S_MANIFEST_DIR/oauth2-proxy-deployment-prole.yaml"
[[ -f "$SECRET_TMPL" ]] || die "missing manifest: $SECRET_TMPL"
[[ -f "$DEPLOY_MANIFEST" ]] || die "missing manifest: $DEPLOY_MANIFEST"
log "==> oauth2-proxy bootstrap (prole.org / k3s)"
log " namespace : $NAMESPACE"
log " kubectx : ${KCTX:-<ambient>}"
log "Applying oauth2-proxy-google-oidc Secret ..."
envsubst '${OAUTH2_PROXY_CLIENT_ID} ${OAUTH2_PROXY_CLIENT_SECRET} ${OAUTH2_PROXY_COOKIE_SECRET}' \
< "$SECRET_TMPL" \
| kubectl "${KCTX_FLAG[@]}" -n "$NAMESPACE" apply -f -
log "Applying oauth2-proxy ServiceAccount + Service + Deployment ..."
kubectl "${KCTX_FLAG[@]}" -n "$NAMESPACE" apply -f "$DEPLOY_MANIFEST"
log "Waiting for oauth2-proxy Deployment to become Ready (timeout 180s) ..."
kubectl "${KCTX_FLAG[@]}" -n "$NAMESPACE" rollout status deployment/oauth2-proxy --timeout=180s
log "==> oauth2-proxy bootstrap complete."
echo ""
echo " Next steps (NOT performed by this script):"
echo " 1. Patch the db.prole.org Ingress/IngressRoute so traffic routes"
echo " through oauth2-proxy:80 instead of supabase-kong:8000 directly."
echo " Check current routing:"
echo " kubectl -n supabase get ingress,ingressroute"
echo " 2. Remove or disable any basic-auth plugin on the Studio route in"
echo " the supabase-kong configmap; rollout-restart supabase-kong."
echo " 3. Browser-test: https://db.prole.org/ → Google sign-in (prole.org"
echo " account). Verify a non-prole.org account receives 403."
echo ""

View File

@ -20,10 +20,13 @@ _has_config=0
for _arg in "$@"; do
[[ "$_arg" == "-c" || "$_arg" == "--config" || "$_arg" == -c=* || "$_arg" == --config=* ]] && _has_config=1
done
if [[ $_has_config -eq 0 && -f "$SCRIPT_DIR/../conf/knoe.cfg" ]]; then
set -- "-c" "$SCRIPT_DIR/../conf/knoe.cfg" "$@"
if [[ $_has_config -eq 0 ]]; then
_default_cfg="$(common_core_default_config_path "$SCRIPT_DIR" || true)"
if [[ -n "$_default_cfg" ]]; then
set -- "-c" "$_default_cfg" "$@"
fi
fi
unset _has_config _arg
unset _has_config _arg _default_cfg
common_core_preparse_config "$@"
@ -100,7 +103,7 @@ root_token_file="$SECRETS_DIR/openbao-root-token"
OPENBAO_NAMESPACE=${OPENBAO_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-default}}}
# OpenBao resources (StatefulSet/Service) live in `OPENBAO_NAMESPACE` (usually the service namespace),
# but secret *paths* must be keyed by the Knoe workload namespace from `conf/knoe.cfg` so that
# but secret *paths* must be keyed by the Knoe workload namespace from the active config file so that
# `${OPENBAO:kv/knoe/<PROLE_NAMESPACE>/...}` references resolve consistently across scripts.
OPENBAO_PATH_NAMESPACE=${OPENBAO_PATH_NAMESPACE:-${PROLE_NAMESPACE:-${NAMESPACE:-default}}}
OPENBAO_RESOURCE_NAMESPACE="$OPENBAO_NAMESPACE"
@ -416,6 +419,85 @@ should_apply_kerberos_configmap() {
return 1
}
iscsi_pv_names_from_manifest() {
local manifest="$1"
[[ -f "$manifest" ]] || return 0
# Extract PV names by scanning PersistentVolume documents.
awk '
$1=="kind:" && $2=="PersistentVolume" {in_pv=1; next}
in_pv && $1=="name:" {print $2; in_pv=0}
' "$manifest" | tr -d $'"\r' | grep -v '^$' || true
}
desired_pv_path_from_manifest() {
local manifest="$1"
local pv_name="$2"
[[ -f "$manifest" ]] || return 0
awk -v pv="$pv_name" '
/^---/ {in_doc=0; is_pv=0; hit=0}
$1=="kind:" && $2=="PersistentVolume" {is_pv=1}
is_pv && $1=="name:" && $2==pv {hit=1}
hit && $1=="path:" {print $2; exit}
' "$manifest" | tr -d $'"\r' | head -1 || true
}
desired_pv_node_from_manifest() {
local manifest="$1"
local pv_name="$2"
[[ -f "$manifest" ]] || return 0
awk -v pv="$pv_name" '
/^---/ {is_pv=0; hit=0; in_values=0}
$1=="kind:" && $2=="PersistentVolume" {is_pv=1}
is_pv && $1=="name:" && $2==pv {hit=1}
hit && $1=="values:" {in_values=1; next}
in_values && $1=="-" {print $2; exit}
' "$manifest" | tr -d $'"\r' | head -1 || true
}
ensure_iscsi_pvs() {
local manifest="$1"
[[ -f "$manifest" ]] || return 0
local pv
for pv in $(iscsi_pv_names_from_manifest "$manifest"); do
if ! kubectl get pv "$pv" >/dev/null 2>&1; then
continue
fi
local desired_path desired_node
desired_path=$(desired_pv_path_from_manifest "$manifest" "$pv")
desired_node=$(desired_pv_node_from_manifest "$manifest" "$pv")
local actual_path actual_node phase
actual_path=$(kubectl get pv "$pv" -o jsonpath='{.spec.local.path}' 2>/dev/null || true)
actual_node=$(kubectl get pv "$pv" -o jsonpath='{.spec.nodeAffinity.required.nodeSelectorTerms[0].matchExpressions[0].values[0]}' 2>/dev/null || true)
phase=$(kubectl get pv "$pv" -o jsonpath='{.status.phase}' 2>/dev/null || true)
local mismatch=0
if [[ -n "$desired_path" && -n "$actual_path" && "$desired_path" != "$actual_path" ]]; then
mismatch=1
fi
if [[ -n "$desired_node" && -n "$actual_node" && "$desired_node" != "$actual_node" ]]; then
mismatch=1
fi
if [[ "$mismatch" == "1" ]]; then
if [[ "$phase" == "Bound" ]]; then
echo "ERROR: PV '$pv' is Bound but differs from desired immutable fields." >&2
echo " actual path=$actual_path node=$actual_node" >&2
echo " desired path=$desired_path node=$desired_node" >&2
echo "Refusing to delete a Bound PV. Resolve by draining workloads / deleting PVCs, then retry." >&2
return 1
fi
echo "Recreating PV '$pv' to match desired local.path/nodeAffinity (phase=${phase:-<unknown>}) ..."
kubectl delete pv "$pv" --ignore-not-found --wait=true --timeout=120s >/dev/null 2>&1 || true
fi
done
echo "Applying iSCSI PersistentVolumes ..."
kubectl apply -f "$manifest"
}
apply_k8s() {
echo "Applying OpenBao manifest to namespace '$OPENBAO_RESOURCE_NAMESPACE' ..."
local use_statefulset=0
@ -423,14 +505,17 @@ apply_k8s() {
use_statefulset=1
fi
if [[ "$use_statefulset" == "1" ]]; then
# Ensure storage class and PVs exist for k3s
if [[ -f "$SCRIPT_DIR/../k8s/knoe/storageclass-synology-iscsi.yaml" ]]; then
echo "Applying StorageClass 'synology-iscsi' ..."
kubectl apply -f "$SCRIPT_DIR/../k8s/knoe/storageclass-synology-iscsi.yaml"
fi
if [[ -f "$SCRIPT_DIR/../k8s/knoe/iscsi-pvs.yaml" ]]; then
echo "Applying iSCSI PersistentVolumes ..."
kubectl apply -f "$SCRIPT_DIR/../k8s/knoe/iscsi-pvs.yaml"
# Ensure storage class and PVs exist for k3s (skipped on GKE/k8s — uses CSI provisioner)
if [[ "${KNOE_MODE:-}" != "k8s" ]]; then
if [[ -f "$SCRIPT_DIR/../k8s/knoe/storageclass-synology-iscsi.yaml" ]]; then
echo "Applying StorageClass 'synology-iscsi' ..."
kubectl apply -f "$SCRIPT_DIR/../k8s/knoe/storageclass-synology-iscsi.yaml"
fi
if [[ -f "$SCRIPT_DIR/../k8s/knoe/iscsi-pvs.yaml" ]]; then
ensure_iscsi_pvs "$SCRIPT_DIR/../k8s/knoe/iscsi-pvs.yaml"
fi
else
echo "[GKE] Skipping Synology iSCSI StorageClass and PVs (not supported on GKE Autopilot; using CSI provisioner)."
fi
local output=""
@ -440,6 +525,20 @@ apply_k8s() {
else
if [[ "${KNOE_MODE:-}" == "k3d" && "$output" == *"updates to statefulset spec"* ]]; then
echo "WARN: OpenBao StatefulSet immutable in k3d; skipping apply."
elif [[ "$output" == *"updates to statefulset spec"* ]]; then
# VolumeClaimTemplates are immutable; delete the StatefulSet (PVCs are orphaned/preserved)
# and recreate so the new storageClass name takes effect.
echo "WARN: OpenBao StatefulSet VolumeClaimTemplates changed; deleting and recreating ..."
kubectl delete statefulset "$OPENBAO_NAME" -n "$OPENBAO_RESOURCE_NAMESPACE" --ignore-not-found >/dev/null 2>&1 || true
local _pvc_sc
_pvc_sc=$(kubectl get pvc "data-openbao-0" -n "$OPENBAO_RESOURCE_NAMESPACE" \
-o jsonpath='{.spec.storageClassName}' 2>/dev/null || true)
if [[ -n "$_pvc_sc" && "$_pvc_sc" != "synology-iscsi" ]]; then
echo " Removing stale PVC 'data-openbao-0' (storageClass: $_pvc_sc) ..."
kubectl delete pvc "data-openbao-0" -n "$OPENBAO_RESOURCE_NAMESPACE" --ignore-not-found >/dev/null 2>&1 || true
fi
knoe_render_manifest "$SCRIPT_DIR/../k8s/knoe/openbao-statefulset.yaml" \
| kubectl apply --validate=false -n "$OPENBAO_RESOURCE_NAMESPACE" -f -
else
echo "$output" >&2
return 1

View File

@ -32,16 +32,16 @@ Usage:
$PROG [-v|--verbose] [-f|--force] [-c|--config-file=FILE] <start|stop|restart|status> [component]
Options:
-c, --config-file=FILE Path to a knoe.cfg file to source when generating port-mapping.cfg
-c, --config-file=FILE Path to a config file to source when generating port-mapping.cfg
-v, --verbose Verbose output
-f, --force Force: kill existing processes blocking ports
Examples:
$PROG -c ./knoe.cfg start
$PROG -c ./k3d.cfg start
$PROG stop openbao
$PROG --verbose status
Config (knoe.cfg or port-mapping.cfg):
Config (k3d.cfg/k3s.cfg/gke.cfg or port-mapping.cfg):
PORT_FORWARD_K3D_MAPPING_1 = id=dashboard;namespace=kubernetes-dashboard;target=svc/kubernetes-dashboard-kong-proxy;address=127.0.0.1;hostPort=8443;servicePort=443;protocol=TCP;description=Kubernetes Dashboard
PORT_FORWARD_K3S_MAPPING_1 = id=opentofu;namespace=\${NAMESPACE};target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu
Legacy port-mapping.cfg:
@ -60,13 +60,22 @@ have() { command -v "$1" >/dev/null 2>&1; }
# ---- Config helpers / derived defaults ----
cfg_file() {
if [ -n "${KNOE_CONF:-}" ] && [ -f "$KNOE_CONF/knoe.cfg" ]; then
printf '%s' "$KNOE_CONF/knoe.cfg"
return 0
fi
if [ -n "${KNOE_HOME:-}" ] && [ -f "$KNOE_HOME/conf/knoe.cfg" ]; then
printf '%s' "$KNOE_HOME/conf/knoe.cfg"
return 0
local cfg=""
if declare -F _knoe_cfg_select_cfg_file >/dev/null 2>&1; then
if [ -n "${KNOE_CONF:-}" ]; then
cfg="$(_knoe_cfg_select_cfg_file "$KNOE_CONF")"
if [ -n "$cfg" ]; then
printf '%s' "$cfg"
return 0
fi
fi
if [ -n "${KNOE_HOME:-}" ]; then
cfg="$(_knoe_cfg_select_cfg_file "$KNOE_HOME/conf")"
if [ -n "$cfg" ]; then
printf '%s' "$cfg"
return 0
fi
fi
fi
return 1
}

170
mock_val/init_redis.sh Executable file
View File

@ -0,0 +1,170 @@
#!/usr/bin/env bash
set -euo pipefail
# init_redis.sh
# Purpose:
# - Deploy Redis as a shared common service via Bitnami Helm chart
# - Provides a pub/sub-capable Redis instance for knoe CLI, desktop, and GitLab
# - Runs in knoe-system (or SERVICE_NAMESPACE) so all services can reach it
# - Service endpoint: redis-master.<namespace>.svc.cluster.local:6379
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Shared option parsing for common core scripts
# shellcheck disable=SC1090
source "$SCRIPT_DIR/common_core_lib.sh"
# Inject default config if not provided
_has_config=0
for _arg in "$@"; do
[[ "$_arg" == "-c" || "$_arg" == "--config" || "$_arg" == -c=* || "$_arg" == --config=* ]] && _has_config=1
done
if [[ $_has_config -eq 0 ]]; then
_default_cfg="$(common_core_default_config_path "$SCRIPT_DIR" || true)"
if [[ -n "$_default_cfg" ]]; then
set -- "-c" "$_default_cfg" "$@"
fi
fi
unset _has_config _arg _default_cfg
common_core_preparse_config "$@"
# shellcheck disable=SC1090
source "$SCRIPT_DIR/knoe_cfg.sh"
set -- "${COMMON_CORE_ARGS[@]}"
common_core_parse_args "$@"
if [[ -z "${KNOE_MODE:-}" ]]; then
export KNOE_MODE="k3s"
fi
if [[ "${COMMON_CORE_HELP:-0}" == 1 ]]; then
common_core_usage "$0"
exit 0
fi
if [[ -n "${COMMON_CORE_PARSE_ERROR:-}" ]]; then
echo "ERROR: ${COMMON_CORE_PARSE_ERROR}" >&2
common_core_usage "$0"
exit 2
fi
ACTION="$COMMON_CORE_ACTION"
NAMESPACE="$(common_core_resolve_namespace "${SERVICE_NAMESPACE:-knoe-system}")"
common_core_apply_namespace "$NAMESPACE"
REDIS_RELEASE="${REDIS_RELEASE:-redis}"
REDIS_REPO_NAME="bitnami"
REDIS_REPO_URL="https://charts.bitnami.com/bitnami"
REDIS_CHART="bitnami/redis"
REDIS_CHART_VERSION="${REDIS_CHART_VERSION:-}"
REDIS_PORT="${REDIS_PORT:-6379}"
REDIS_PVC_SIZE="${REDIS_PVC_SIZE:-1Gi}"
# No dynamic provisioner is available in the service cluster; persistence disabled by default
# (Redis is used as a pub/sub broker — durable storage is not required)
REDIS_STORAGE_CLASS="${REDIS_STORAGE_CLASS:-${STORAGE_CLASS:-}}"
REDIS_PERSISTENCE_ENABLED="${REDIS_PERSISTENCE_ENABLED:-false}"
# Image registry override — set to "registry.bitnami.com" if docker.io is unreachable
# Bitnami OCI registry works as an alternative when Docker Hub times out on constrained networks.
REDIS_IMAGE_REGISTRY="${REDIS_IMAGE_REGISTRY:-}"
# Helm timeout — increase for slow image pulls on constrained network links (e.g. Pi cluster)
REDIS_HELM_TIMEOUT="${REDIS_HELM_TIMEOUT:-8m}"
log() { echo "[INFO] $*"; }
warn() { echo "[WARN] $*" >&2; }
die() { echo "[ERROR] $*" >&2; exit 1; }
command -v helm >/dev/null 2>&1 || die "helm not found"
command -v kubectl >/dev/null 2>&1 || die "kubectl not found"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
redis_service_host() {
printf '%s-master.%s.svc.cluster.local' "$REDIS_RELEASE" "$NAMESPACE"
}
redis_is_running() {
kubectl -n "$NAMESPACE" get deployment "${REDIS_RELEASE}-master" >/dev/null 2>&1 || \
kubectl -n "$NAMESPACE" get statefulset "${REDIS_RELEASE}-master" >/dev/null 2>&1
}
# ---------------------------------------------------------------------------
# Deploy
# ---------------------------------------------------------------------------
deploy_redis() {
# If Redis is already running and REDIS_FORCE_UPGRADE is not set, skip the
# helm upgrade to avoid Docker Hub / OCI registry timeouts on constrained
# networks (e.g. Pi cluster where registry-1.docker.io may be unreachable).
# Set REDIS_FORCE_UPGRADE=1 to force a chart upgrade regardless.
if [[ "${REDIS_FORCE_UPGRADE:-0}" != "1" ]] && redis_is_running; then
log "Redis already running in '${NAMESPACE}'; skipping upgrade."
log "Set REDIS_FORCE_UPGRADE=1 to force a helm upgrade."
return 0
fi
log "Adding/updating Bitnami Helm repo..."
helm repo add "$REDIS_REPO_NAME" "$REDIS_REPO_URL" 2>&1 || true
helm repo update "$REDIS_REPO_NAME" 2>&1 || warn "helm repo update returned non-zero; continuing..."
kubectl get namespace "$NAMESPACE" >/dev/null 2>&1 || kubectl create namespace "$NAMESPACE" >/dev/null
local helm_args=(
upgrade --install "$REDIS_RELEASE" "$REDIS_CHART"
--namespace "$NAMESPACE"
--create-namespace
--timeout "$REDIS_HELM_TIMEOUT"
--wait
--set architecture=standalone
--set auth.enabled=false
--set master.persistence.enabled="${REDIS_PERSISTENCE_ENABLED}"
--set master.persistence.size="$REDIS_PVC_SIZE"
--set master.resources.requests.memory=128Mi
--set master.resources.requests.cpu=50m
--set master.resources.limits.memory=512Mi
--set master.resources.limits.cpu=500m
)
if [[ -n "$REDIS_CHART_VERSION" ]]; then
helm_args+=(--version "$REDIS_CHART_VERSION")
fi
if [[ -n "$REDIS_STORAGE_CLASS" ]]; then
helm_args+=(--set master.persistence.storageClass="$REDIS_STORAGE_CLASS")
fi
if [[ -n "$REDIS_IMAGE_REGISTRY" ]]; then
log "Using image registry override: ${REDIS_IMAGE_REGISTRY}"
helm_args+=(--set global.imageRegistry="$REDIS_IMAGE_REGISTRY")
fi
log "Deploying Redis (release=${REDIS_RELEASE}, ns=${NAMESPACE})..."
helm "${helm_args[@]}"
log "Redis ready. Endpoint: $(redis_service_host):${REDIS_PORT}"
log "Pub/sub: redis-cli -h $(redis_service_host) -p ${REDIS_PORT} subscribe <channel>"
}
# ---------------------------------------------------------------------------
# Action dispatch
# ---------------------------------------------------------------------------
case "$ACTION" in
start|update|reload|initialize|restart)
deploy_redis
;;
stop)
log "Removing Redis (${REDIS_RELEASE}) from namespace ${NAMESPACE}..."
helm -n "$NAMESPACE" uninstall "$REDIS_RELEASE" 2>/dev/null || warn "Redis release not found."
;;
status)
log "Redis status in namespace ${NAMESPACE}:"
kubectl -n "$NAMESPACE" get pods -l "app.kubernetes.io/name=redis" 2>/dev/null || \
echo " No Redis pods found."
kubectl -n "$NAMESPACE" get svc -l "app.kubernetes.io/name=redis" 2>/dev/null || true
;;
*)
die "Unknown action: $ACTION"
;;
esac

View File

@ -19,10 +19,13 @@ _has_config=0
for _arg in "$@"; do
[[ "$_arg" == "-c" || "$_arg" == "--config" || "$_arg" == -c=* || "$_arg" == --config=* ]] && _has_config=1
done
if [[ $_has_config -eq 0 && -f "$SCRIPT_DIR/../conf/knoe.cfg" ]]; then
set -- "-c" "$SCRIPT_DIR/../conf/knoe.cfg" "$@"
if [[ $_has_config -eq 0 ]]; then
_default_cfg="$(common_core_default_config_path "$SCRIPT_DIR" || true)"
if [[ -n "$_default_cfg" ]]; then
set -- "-c" "$_default_cfg" "$@"
fi
fi
unset _has_config _arg
unset _has_config _arg _default_cfg
common_core_preparse_config "$@"
@ -55,13 +58,21 @@ REGISTRY_MANIFEST_FILE=${REGISTRY_MANIFEST_FILE:-"$REGISTRY_MANIFEST_DIR/deploym
usage() {
cat <<EOF
Usage: init_registry.sh [-n|--namespace NS] [-r|--registry-namespace NS] <start|stop|status|restart|initialize|update|reload>
Usage: init_registry.sh [-n|--namespace NS] [-r|--registry-namespace NS] <start|stop|status|restart|initialize|update|reload|migrate>
Deploys registry support for local clusters.
- In k3s mode, deploys `registry:2` into the service namespace (default: SERVICE_NAMESPACE).
- In k3d mode, manages the `k3d` registry (port 5000) and removes any in-cluster registry resources.
Actions:
start|initialize|update|reload|restart Deploy or update the registry
stop Remove registry resources
status Show registry pod/service status
migrate Copy all images from registry:2 → gitlab-registry.
Run AFTER GitLab is up, BEFORE 'stop'.
Uses skopeo if available, otherwise prints commands.
NOTE: `-r/--registry-namespace` is kept for backwards compatibility and is treated as an alias for `-n/--namespace`.
EOF
}
@ -125,7 +136,11 @@ elif [[ -n "${REGISTRY_NAMESPACE:-}" ]]; then
REGISTRY_NAMESPACE="$REGISTRY_NAMESPACE"
else
# Default to the configured service namespace (driven by knoe.cfg via knoe_cfg.sh).
REGISTRY_NAMESPACE="${SERVICE_NAMESPACE:-${PROLE_NAMESPACE:-default}}"
REGISTRY_NAMESPACE="${SERVICE_NAMESPACE:-${PROLE_NAMESPACE:-}}"
if [[ -z "$REGISTRY_NAMESPACE" ]]; then
echo "ERROR: REGISTRY_NAMESPACE could not be determined. Set SERVICE_NAMESPACE in knoe.cfg or pass -n/--namespace." >&2
exit 1
fi
fi
# In k3s mode, the in-cluster registry is a common core service and should live
@ -134,8 +149,12 @@ _mode_resolved="${KNOE_MODE:-${DEPLOYMENT_MODE:-}}"
if declare -F knoe_normalize_mode >/dev/null 2>&1; then
_mode_resolved="$(knoe_normalize_mode "$_mode_resolved")"
fi
if [[ "$_mode_resolved" == "k3s" && ( -z "${REGISTRY_NAMESPACE:-}" || "${REGISTRY_NAMESPACE}" == "default" ) ]]; then
REGISTRY_NAMESPACE="${SERVICE_NAMESPACE:-${PROLE_NAMESPACE:-default}}"
if [[ "$_mode_resolved" == "k3s" && -z "${REGISTRY_NAMESPACE:-}" ]]; then
REGISTRY_NAMESPACE="${SERVICE_NAMESPACE:-${PROLE_NAMESPACE:-}}"
if [[ -z "$REGISTRY_NAMESPACE" ]]; then
echo "ERROR: REGISTRY_NAMESPACE could not be determined for k3s mode. Set SERVICE_NAMESPACE in knoe.cfg or pass -n/--namespace." >&2
exit 1
fi
fi
unset _mode_resolved
@ -170,7 +189,7 @@ resolve_registry_node_selector() {
return 0
fi
# Default: pin to the control-plane node so hostPort:5000 is reachable via
# the k3s server host (e.g. myrddin.knoe.org:5000).
# the k3s server host (e.g. myrddin.prole.org:5000).
resolve_control_plane_node_selector
}
@ -323,8 +342,26 @@ apply_registry() {
mode=$(current_mode)
if [[ "$mode" == "k3d" ]]; then
if command -v k3d >/dev/null 2>&1; then
# Remove legacy prole-registry that may be squatting on port 5000.
# It may be k3d-managed (delete via k3d) or a bare Docker container (rm -f).
if k3d registry list prole-registry >/dev/null 2>&1 \
|| docker inspect k3d-prole-registry >/dev/null 2>&1; then
echo "Removing legacy k3d-prole-registry from port 5000 ..."
k3d registry delete prole-registry >/dev/null 2>&1 || true
docker stop k3d-prole-registry >/dev/null 2>&1 || true
docker rm -f k3d-prole-registry >/dev/null 2>&1 || true
fi
echo "Ensuring k3d registry 'knoe-registry' on port 5000 ..."
if ! k3d registry list knoe-registry >/dev/null 2>&1; then
# If knoe-registry was created but never started (port was taken), nuke and recreate.
if k3d registry list knoe-registry >/dev/null 2>&1; then
if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -qE '^k3d-knoe-registry$'; then
echo "k3d-knoe-registry exists but is not running; recreating ..."
k3d registry delete knoe-registry >/dev/null 2>&1 || true
docker rm -f k3d-knoe-registry >/dev/null 2>&1 || true
k3d registry create knoe-registry --port 5000 || true
fi
else
k3d registry create knoe-registry --port 5000 || true
fi
local cluster_name
@ -377,6 +414,30 @@ apply_registry() {
fi
fi
# In k3s mode, if GitLab is deployed it owns port 5000 (gitlab-registry).
# Deploying registry:2 with hostPort:5000 on the same node would conflict.
# Skip the apply and warn; use init_registry.sh stop to remove the old deployment.
local _mode_for_gitlab_check
_mode_for_gitlab_check=$(current_mode)
if [[ "$_mode_for_gitlab_check" == "k3s" ]]; then
local _gitlab_ns="${GITLAB_NAMESPACE:-gitlab}"
if kubectl get namespace "$_gitlab_ns" >/dev/null 2>&1; then
echo "WARN: GitLab namespace '${_gitlab_ns}' detected in k3s mode." >&2
echo " gitlab-registry owns port 5000 — registry:2 deployment skipped." >&2
echo " Run 'init_registry.sh stop' to remove any existing registry:2 resources." >&2
return 0
fi
fi
# Idempotency check: skip apply+wait if the registry is already available.
local _desired _available
_desired=$(kubectl get deploy/registry -n "$REGISTRY_NAMESPACE" -o jsonpath='{.spec.replicas}' 2>/dev/null || true)
_available=$(kubectl get deploy/registry -n "$REGISTRY_NAMESPACE" -o jsonpath='{.status.availableReplicas}' 2>/dev/null || true)
if [[ -n "$_desired" && "${_available:-0}" -ge "${_desired:-1}" && "${_desired:-0}" -gt 0 ]]; then
echo "Registry already running in namespace '$REGISTRY_NAMESPACE' ($_available/$_desired replicas available); skipping apply."
return 0
fi
echo "Applying registry manifest to namespace '$REGISTRY_NAMESPACE' ..."
render_registry_manifest | kubectl apply --server-side --force-conflicts --field-manager=knoe-installer --validate=false -n "$REGISTRY_NAMESPACE" -f -
apply_registry_node_selector
@ -415,6 +476,72 @@ status_registry() {
kubectl -n "$REGISTRY_NAMESPACE" get pods -l "app=registry" 2>/dev/null || true
}
migrate_registry_to_gitlab() {
# Enumerate images from the registry:2 instance (registry.<src_ns>.svc.cluster.local:5000)
# and copy them to gitlab-registry using skopeo when available.
# Safe to run multiple times — skopeo copy is idempotent.
# Skipped gracefully if registry:2 has no images or is already gone.
local src_ns="${REGISTRY_NAMESPACE:-knoe-system}"
local gitlab_ns="${GITLAB_NAMESPACE:-gitlab}"
local src_registry="${REGISTRY_MIGRATE_SRC:-registry.${src_ns}.svc.cluster.local:5000}"
local dst_registry="${REGISTRY_MIGRATE_DST:-gitlab-registry.${gitlab_ns}.svc.cluster.local:5000}"
echo "Registry migration: ${src_registry}${dst_registry}"
# Verify the source registry:2 pod exists at all before trying to catalog it.
if ! kubectl -n "$src_ns" get deploy/registry >/dev/null 2>&1 && \
! kubectl -n "$src_ns" get pods -l app=registry --field-selector=status.phase=Running 2>/dev/null | grep -q Running; then
echo "INFO: registry:2 not found in namespace '${src_ns}' — nothing to migrate."
return 0
fi
# Fetch repository catalog via a temporary curl pod on the cluster.
local repos
repos=$(kubectl -n "$src_ns" run registry-catalog-migrate \
--image=alpine/curl:latest --restart=Never --rm --attach --quiet \
--overrides="{\"spec\":{\"tolerations\":[{\"operator\":\"Exists\"}],\"containers\":[{\"name\":\"c\",\"image\":\"alpine/curl:latest\",\"command\":[\"sh\",\"-c\",\"curl -sf http://${src_registry}/v2/_catalog\"]}]}}" \
2>/dev/null \
| python3 -c "import sys,json; [print(r) for r in json.load(sys.stdin).get('repositories',[])]" 2>/dev/null || true)
if [[ -z "$repos" ]]; then
echo "INFO: No repositories found in registry:2 at ${src_registry} — nothing to migrate."
return 0
fi
echo "Repositories to migrate:"
printf '%s\n' "$repos" | sed 's/^/ /'
if command -v skopeo >/dev/null 2>&1; then
echo "skopeo found — migrating images automatically..."
local _ok=0 _fail=0
while IFS= read -r repo; do
[[ -z "$repo" ]] && continue
echo " Copying ${repo} ..."
if skopeo copy --all \
"docker://${src_registry}/${repo}" \
"docker://${dst_registry}/${repo}" \
--dest-tls-verify=false --src-tls-verify=false 2>&1; then
echo " [OK] ${repo}"
(( _ok++ )) || true
else
echo " [WARN] ${repo} — copy failed, may need manual retry"
(( _fail++ )) || true
fi
done <<< "$repos"
echo "Migration complete: ${_ok} succeeded, ${_fail} failed."
if [[ $_fail -gt 0 ]]; then
echo "Failed repos can be retried with: REGISTRY_MIGRATE_SRC=${src_registry} REGISTRY_MIGRATE_DST=${dst_registry} ./etc/init_registry.sh migrate"
fi
else
echo ""
echo "skopeo not found — run these commands manually (install: sudo apt-get install skopeo):"
while IFS= read -r repo; do
[[ -z "$repo" ]] && continue
echo " skopeo copy --all docker://${src_registry}/${repo} docker://${dst_registry}/${repo} --dest-tls-verify=false --src-tls-verify=false"
done <<< "$repos"
fi
}
case "${ACTION:-}" in
start|initialize|update|reload|restart)
ensure_tools
@ -437,6 +564,12 @@ case "${ACTION:-}" in
status_registry
fi
;;
migrate)
# Migrate images from registry:2 → gitlab-registry before decommissioning registry:2.
# Run after GitLab is up and before running 'stop' on the old registry.
ensure_tools
migrate_registry_to_gitlab
;;
*)
usage >&2
exit 1

View File

@ -4,7 +4,7 @@ set -euo pipefail
# init_service_layer.sh
# Purpose:
# - Deploy the Knoe service layer (OpenTofu, Garage, OpenBao, Kong; Kerberos optional)
# - Deploy the service layer (Garage, OpenBao, Kong; OpenTofu in non-k8s modes)
# - Keep service-layer resources grouped in SERVICE_NAMESPACE
# - Migrate service layer to a new namespace
@ -18,10 +18,13 @@ _has_config=0
for _arg in "$@"; do
[[ "$_arg" == "-c" || "$_arg" == "--config" || "$_arg" == -c=* || "$_arg" == --config=* ]] && _has_config=1
done
if [[ $_has_config -eq 0 && -f "$SCRIPT_DIR/../conf/knoe.cfg" ]]; then
set -- "-c" "$SCRIPT_DIR/../conf/knoe.cfg" "$@"
if [[ $_has_config -eq 0 ]]; then
_default_cfg="$(common_core_default_config_path "$SCRIPT_DIR" || true)"
if [[ -n "$_default_cfg" ]]; then
set -- "-c" "$_default_cfg" "$@"
fi
fi
unset _has_config _arg
unset _has_config _arg _default_cfg
common_core_preparse_config "$@"
@ -29,7 +32,7 @@ common_core_preparse_config "$@"
source "$SCRIPT_DIR/knoe_cfg.sh"
knoe_ensure_kubeconfig >/dev/null 2>&1 || true
knoe_ensure_kube_context || exit 1
ensure_kube_context || exit 1
ACTION=""
SERVICE_NAMESPACE_OVERRIDE=""
@ -169,11 +172,16 @@ deploy_service_layer() {
local action="$1"
local ns="$2"
local rc=0
local manage_opentofu=1
if [[ "${KNOE_MODE:-}" == "k8s" ]]; then
manage_opentofu=0
fi
ensure_namespace "$ns"
label_namespace "$ns"
local argocd_action opentofu_action garage_action kdc_action openbao_action kong_action registry_action
local argocd_action opentofu_action garage_action kdc_action openbao_action redis_action kong_action registry_action
case "$action" in
start|initialize|update|reload) argocd_action="update" ;;
restart) argocd_action="restart" ;;
@ -182,13 +190,15 @@ deploy_service_layer() {
*) argocd_action="update" ;;
esac
case "$action" in
start|initialize|update|reload) opentofu_action="update" ;;
restart) opentofu_action="restart" ;;
stop) opentofu_action="stop" ;;
status) opentofu_action="status" ;;
*) opentofu_action="update" ;;
esac
if [[ "$manage_opentofu" == "1" ]]; then
case "$action" in
start|initialize|update|reload) opentofu_action="update" ;;
restart) opentofu_action="restart" ;;
stop) opentofu_action="stop" ;;
status) opentofu_action="status" ;;
*) opentofu_action="update" ;;
esac
fi
case "$action" in
start|initialize|update|reload) openbao_action="update" ;;
@ -198,6 +208,14 @@ deploy_service_layer() {
*) openbao_action="update" ;;
esac
case "$action" in
start|initialize|update|reload) redis_action="update" ;;
restart) redis_action="restart" ;;
stop) redis_action="stop" ;;
status) redis_action="status" ;;
*) redis_action="update" ;;
esac
case "$action" in
start|initialize|update|reload) garage_action="start" ;;
restart) garage_action="restart" ;;
@ -239,7 +257,7 @@ deploy_service_layer() {
# 2. OpenBao secrets vault; needed by downstream services
# 3. Garage object storage
# 4. Kong API gateway
# 5. OpenTofu IaC engine; depends on registry + secrets (last)
# 5. OpenTofu IaC engine; depends on registry + secrets (non-k8s, last)
# -------------------------------------------------------------------------
if [[ -x "$SCRIPT_DIR/init_registry.sh" ]]; then
@ -250,6 +268,14 @@ deploy_service_layer() {
OPENBAO_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_openbao.sh" -n "$ns" "$openbao_action" || rc=$?
# Redis — shared pub/sub broker; deploy before Kong so GitLab and knoe services can reach it
if [[ -x "$SCRIPT_DIR/init_redis.sh" ]]; then
REDIS_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_redis.sh" -n "$ns" "$redis_action" || rc=$?
else
log "[WARN] init_redis.sh not found; Redis deploy skipped."
fi
# cert-manager is cluster-scoped and managed independently via init_certmgr.sh
# in its own dedicated 'cert-manager' namespace; it is not part of the service layer.
@ -259,12 +285,21 @@ deploy_service_layer() {
KONG_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_kong.sh" -n "$ns" "$kong_action" || rc=$?
OPENTOFU_NAMESPACE="$ns" OPENTOFU_SECRET_NAMESPACE="${NAMESPACE:-$ns}" OPENTOFU_OPENBAO_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_opentofu.sh" -n "$ns" "$opentofu_action" || rc=$?
if [[ "$manage_opentofu" == "1" ]]; then
OPENTOFU_NAMESPACE="$ns" OPENTOFU_SECRET_NAMESPACE="${NAMESPACE:-$ns}" OPENTOFU_OPENBAO_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_opentofu.sh" -n "$ns" "$opentofu_action" || rc=$?
else
log "[INFO] k8s mode: skipping OpenTofu deploy."
fi
if [[ "$ENABLE_KERBEROS" == "1" ]]; then
SERVICE_NAMESPACE="$ns" PROLE_KDC_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_kdc.sh" "$kdc_action" || rc=$?
# KDC is embedded in `knoe-auth` by default. Only deploy standalone KDC when requested.
if [[ "${PROLE_KDC_STANDALONE:-0}" == "1" ]]; then
SERVICE_NAMESPACE="$ns" PROLE_KDC_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_kdc.sh" "$kdc_action" || rc=$?
else
log "[INFO] Kerberos enabled: skipping standalone KDC deploy (KDC runs as sidecar in 'knoe-auth')."
fi
fi
return "$rc"
@ -286,13 +321,17 @@ cleanup_old_namespace() {
"$SCRIPT_DIR/init_garage_store.sh" stop || true
OPENBAO_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_openbao.sh" -n "$ns" stop || true
if [[ -x "$SCRIPT_DIR/init_redis.sh" ]]; then
REDIS_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_redis.sh" -n "$ns" stop || true
fi
CERTMGR_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_certmgr.sh" -n "$ns" stop || true
if [[ -x "$SCRIPT_DIR/init_registry.sh" ]]; then
REGISTRY_NAMESPACE="$REGISTRY_NS" SERVICE_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_registry.sh" -n "$REGISTRY_NS" stop || true
fi
if [[ "$ENABLE_KERBEROS" == "1" ]]; then
if [[ "$ENABLE_KERBEROS" == "1" && "${PROLE_KDC_STANDALONE:-0}" == "1" ]]; then
SERVICE_NAMESPACE="$ns" PROLE_KDC_NAMESPACE="$ns" \
"$SCRIPT_DIR/init_kdc.sh" cleanup || true
fi