mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 11:03:59 +00:00
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:
parent
11064cbd5b
commit
3f96f66a78
79
mock_val/init_1password.sh
Executable file
79
mock_val/init_1password.sh
Executable 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"
|
||||
@ -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"
|
||||
;;
|
||||
|
||||
@ -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
@ -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
521
mock_val/init_cnpg_gke.sh
Executable 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"
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
309
mock_val/init_gitea.sh
Normal file → Executable 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
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
99
mock_val/init_grafana_oauth.sh
Executable 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 ""
|
||||
100
mock_val/init_grafana_oauth_prole.sh
Normal file
100
mock_val/init_grafana_oauth_prole.sh
Normal 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 ""
|
||||
@ -26,7 +26,13 @@ K3S_REGISTRY_HOST=${K3S_REGISTRY_HOST:-$(registry_host_from_url "${PROLE_K3S_SER
|
||||
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:
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
573
mock_val/init_knoe_auth.sh
Executable 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
1360
mock_val/init_knoe_users.sh
Executable file
File diff suppressed because it is too large
Load Diff
@ -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
52
mock_val/init_min.sh
Normal 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
|
||||
@ -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
|
||||
|
||||
# Don’t 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
|
||||
|
||||
@ -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
127
mock_val/init_oauth2_proxy.sh
Executable 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."
|
||||
116
mock_val/init_oauth2_proxy_prole.sh
Normal file
116
mock_val/init_oauth2_proxy_prole.sh
Normal 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 ""
|
||||
@ -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
|
||||
|
||||
@ -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
170
mock_val/init_redis.sh
Executable 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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user