mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 15:54:32 +00:00
checkpoint: improve init scripts, installer flows, and kube context handling
- Fix kube context switching for k3s single-context kubeconfigs and k3d shorthand prefixes - Update common init/status scripts (registry, kerberos, cnpg backup, service layer, common services) - Add Gitea init script and installer ArgoCD screen - Add Supabase realtime probe patching plus regression tests - Extend installer core/UI test coverage
This commit is contained in:
parent
92e4101403
commit
ce5ff83eb4
@ -1,3 +1,16 @@
|
|||||||
# Port mappings for Prole Tools (generated).
|
# Port mappings for Prole Tools (generated).
|
||||||
# Format: key: local=... remote=... ns=... svc=... address=...
|
# Format: key: local=... remote=... ns=... svc=... address=...
|
||||||
|
|
||||||
|
argocd: local=8081 remote=80 ns=argocd svc=argocd-server address=0.0.0.0
|
||||||
|
supabase-studio: local=18080 remote=3000 ns=supabase svc=studio address=0.0.0.0
|
||||||
|
supabase-auth: local=9999 remote=9999 ns=supabase svc=auth address=127.0.0.1
|
||||||
|
supabase-rest: local=3001 remote=3000 ns=supabase svc=rest address=0.0.0.0
|
||||||
|
supabase-realtime: local=4000 remote=4000 ns=supabase svc=realtime address=0.0.0.0
|
||||||
|
garage: local=3900 remote=3900 ns=knoe-system svc=garage address=0.0.0.0
|
||||||
|
openbao: local=8200 remote=8200 ns=knoe-system svc=openbao address=0.0.0.0
|
||||||
|
opentofu: local=8080 remote=8080 ns=knoe-system svc=opentofu address=0.0.0.0
|
||||||
|
dashboard: local=8443 remote=443 ns=kubernetes-dashboard svc=kubernetes-dashboard-kong-proxy address=127.0.0.1
|
||||||
|
postgres: local=5432 remote=5432 ns=knoe-db svc=prole-db-rw address=0.0.0.0
|
||||||
|
prometheus: local=9090 remote=9090 ns=monitoring svc=kps-kube-prometheus-stack-prometheus address=127.0.0.1
|
||||||
|
grafana: local=3000 remote=80 ns=monitoring svc=kps-grafana address=0.0.0.0
|
||||||
|
supabase-kong: local=8000 remote=8000 ns=supabase svc=kong address=0.0.0.0
|
||||||
|
|||||||
@ -78,7 +78,15 @@ fi
|
|||||||
CNPG_MANIFEST="$K8S_PROLE_DIR/prole-db.yaml"
|
CNPG_MANIFEST="$K8S_PROLE_DIR/prole-db.yaml"
|
||||||
BARMAN_OBJECTSTORE_MANIFEST="$K8S_PROLE_DIR/prole-db-barman-objectstore.yaml"
|
BARMAN_OBJECTSTORE_MANIFEST="$K8S_PROLE_DIR/prole-db-barman-objectstore.yaml"
|
||||||
|
|
||||||
SECRETS_DIR="$PROLE_SERVICE/secrets"
|
# Secrets and token locations
|
||||||
|
# Use PROLE_SERVICE if writable locally, otherwise fallback to ~/.prole
|
||||||
|
if [[ -n "${PROLE_SERVICE:-}" ]] && _prole_usable_dir "${PROLE_SERVICE}/secrets" >/dev/null; then
|
||||||
|
SECRETS_DIR="$PROLE_SERVICE/secrets"
|
||||||
|
else
|
||||||
|
SECRETS_DIR="$HOME/.prole/etc/secrets"
|
||||||
|
fi
|
||||||
|
mkdir -p "$SECRETS_DIR" 2>/dev/null || true
|
||||||
|
|
||||||
# Resolving CNPG admin keys.
|
# Resolving CNPG admin keys.
|
||||||
# We prefer names without algorithm suffixes to be more generic, matching install.py fallback strategy.
|
# We prefer names without algorithm suffixes to be more generic, matching install.py fallback strategy.
|
||||||
ADMIN_PRIV_ED25519="$SECRETS_DIR/admin_ed25519.key"
|
ADMIN_PRIV_ED25519="$SECRETS_DIR/admin_ed25519.key"
|
||||||
@ -101,6 +109,40 @@ ensure_tools() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensure_local_admin_keypair() {
|
||||||
|
mkdir -p "$SECRETS_DIR" 2>/dev/null || true
|
||||||
|
|
||||||
|
# If generic already exists, prefer it.
|
||||||
|
if [[ -f "$ADMIN_PRIV_GENERIC" && -f "$ADMIN_PUB_GENERIC" ]]; then
|
||||||
|
# Ensure legacy mirror exists for compatibility.
|
||||||
|
cp "$ADMIN_PRIV_GENERIC" "$ADMIN_PRIV_ED25519" 2>/dev/null || true
|
||||||
|
cp "$ADMIN_PUB_GENERIC" "$ADMIN_PUB_ED25519" 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If legacy exists, normalize it into generic.
|
||||||
|
if [[ -f "$ADMIN_PRIV_ED25519" && -f "$ADMIN_PUB_ED25519" ]]; then
|
||||||
|
cp "$ADMIN_PRIV_ED25519" "$ADMIN_PRIV_GENERIC" 2>/dev/null || true
|
||||||
|
cp "$ADMIN_PUB_ED25519" "$ADMIN_PUB_GENERIC" 2>/dev/null || true
|
||||||
|
chmod 0600 "$ADMIN_PRIV_GENERIC" 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Generating CNPG admin keypair in $SECRETS_DIR ..."
|
||||||
|
# Attempt ed25519, fallback to rsa if not available
|
||||||
|
if ! openssl genpkey -algorithm ED25519 -out "$ADMIN_PRIV_GENERIC" 2>/dev/null; then
|
||||||
|
echo "WARN: ED25519 not supported by openssl, falling back to RSA 4096..." >&2
|
||||||
|
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out "$ADMIN_PRIV_GENERIC"
|
||||||
|
fi
|
||||||
|
openssl pkey -in "$ADMIN_PRIV_GENERIC" -pubout -out "$ADMIN_PUB_GENERIC"
|
||||||
|
chmod 0600 "$ADMIN_PRIV_GENERIC"
|
||||||
|
|
||||||
|
# Mirror to legacy filenames for compatibility with other scripts.
|
||||||
|
cp "$ADMIN_PRIV_GENERIC" "$ADMIN_PRIV_ED25519" 2>/dev/null || true
|
||||||
|
cp "$ADMIN_PUB_GENERIC" "$ADMIN_PUB_ED25519" 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
_kubectl_is_transient_error() {
|
_kubectl_is_transient_error() {
|
||||||
local msg="${1:-}"
|
local msg="${1:-}"
|
||||||
# Errors we frequently see on k3s under load / during restart.
|
# Errors we frequently see on k3s under load / during restart.
|
||||||
@ -1298,8 +1340,12 @@ wait_for_cnpg_pods() {
|
|||||||
|
|
||||||
if [[ "$ready_pods" -ge "$target_pods" ]]; then
|
if [[ "$ready_pods" -ge "$target_pods" ]]; then
|
||||||
echo "All $ready_pods/$target_pods pods are Ready."
|
echo "All $ready_pods/$target_pods pods are Ready."
|
||||||
# Verify DB connectivity if kubectl-cnpg is available
|
# By default, pod readiness is sufficient. Connectivity checks can be enabled explicitly.
|
||||||
|
# This avoids unnecessary timeouts/resets during initial bootstrap when CNPG may still be
|
||||||
|
# initializing even though pods are already Ready.
|
||||||
if kubectl cnpg version >/dev/null 2>&1; then
|
if kubectl cnpg version >/dev/null 2>&1; then
|
||||||
|
local require_psql="${CNPG_REQUIRE_PSQL:-false}"
|
||||||
|
if [[ "$require_psql" == "1" || "$require_psql" == "true" || "$require_psql" == "True" || "$require_psql" == "yes" || "$require_psql" == "YES" ]]; then
|
||||||
echo "Verifying database connectivity via 'kubectl cnpg psql'..."
|
echo "Verifying database connectivity via 'kubectl cnpg psql'..."
|
||||||
if kubectl cnpg -n "$NAMESPACE" psql "$CNPG_CLUSTER_NAME" -- -c "SELECT 1" >/dev/null 2>&1; then
|
if kubectl cnpg -n "$NAMESPACE" psql "$CNPG_CLUSTER_NAME" -- -c "SELECT 1" >/dev/null 2>&1; then
|
||||||
echo "Database connection verified."
|
echo "Database connection verified."
|
||||||
@ -1307,6 +1353,13 @@ wait_for_cnpg_pods() {
|
|||||||
else
|
else
|
||||||
echo " [${elapsed}s/${timeout}s] Pods are Ready but database connection failed — waiting for database to accept connections..."
|
echo " [${elapsed}s/${timeout}s] Pods are Ready but database connection failed — waiting for database to accept connections..."
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
# Best-effort connectivity check (warn-only)
|
||||||
|
if ! kubectl cnpg -n "$NAMESPACE" psql "$CNPG_CLUSTER_NAME" -- -c "SELECT 1" >/dev/null 2>&1; then
|
||||||
|
echo "WARN: CNPG pods are Ready but 'kubectl cnpg psql' is not yet available; continuing." >&2
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
# Fallback if no cnpg plugin: just return 0 if pods are ready
|
# Fallback if no cnpg plugin: just return 0 if pods are ready
|
||||||
return 0
|
return 0
|
||||||
@ -1793,19 +1846,14 @@ fetch_admin_keys_and_db_pass_from_bao_or_local() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
ensure_local_admin_keypair
|
||||||
if [[ -f "$ADMIN_PRIV_GENERIC" && -f "$ADMIN_PUB_GENERIC" ]]; then
|
if [[ -f "$ADMIN_PRIV_GENERIC" && -f "$ADMIN_PUB_GENERIC" ]]; then
|
||||||
echo "Using local admin key pair at $SECRETS_DIR"
|
echo "Using local admin key pair at $SECRETS_DIR"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
if [[ -f "$ADMIN_PRIV_ED25519" && -f "$ADMIN_PUB_ED25519" ]]; then
|
|
||||||
echo "Using local legacy admin key pair at $SECRETS_DIR"
|
echo "ERROR: Could not obtain admin key pair from OpenBao and could not generate local keys." >&2
|
||||||
# Link or copy to generic for consistent use below
|
return 1
|
||||||
cp "$ADMIN_PRIV_ED25519" "$ADMIN_PRIV_GENERIC"
|
|
||||||
cp "$ADMIN_PUB_ED25519" "$ADMIN_PUB_GENERIC"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
echo "ERROR: Could not obtain admin key pair from OpenBao and no local files found." >&2
|
|
||||||
exit 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_cnpg_admin_secret() {
|
apply_cnpg_admin_secret() {
|
||||||
|
|||||||
@ -49,6 +49,9 @@ BACKUP_STATUS_TIMEOUT=${BACKUP_STATUS_TIMEOUT:-600}
|
|||||||
BACKUP_STATUS_INTERVAL=${BACKUP_STATUS_INTERVAL:-10}
|
BACKUP_STATUS_INTERVAL=${BACKUP_STATUS_INTERVAL:-10}
|
||||||
PLUGIN_READY_TIMEOUT=${PLUGIN_READY_TIMEOUT:-180}
|
PLUGIN_READY_TIMEOUT=${PLUGIN_READY_TIMEOUT:-180}
|
||||||
|
|
||||||
|
PLUGIN_SOCKET_DIR=${PLUGIN_SOCKET_DIR:-/plugins}
|
||||||
|
LAST_BACKUP_NAME=""
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<USAGE
|
cat <<USAGE
|
||||||
Usage: $0 [start|backup|status]
|
Usage: $0 [start|backup|status]
|
||||||
@ -329,8 +332,10 @@ remove_native_barman_config() {
|
|||||||
|
|
||||||
wait_for_plugin_ready() {
|
wait_for_plugin_ready() {
|
||||||
local start_time now plugin_names deployment_rows ns ready replicas
|
local start_time now plugin_names deployment_rows ns ready replicas
|
||||||
|
local plugin_deploy_ready pod socket_path
|
||||||
start_time=$(date +%s)
|
start_time=$(date +%s)
|
||||||
while true; do
|
while true; do
|
||||||
|
plugin_deploy_ready=0
|
||||||
plugin_names=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.spec.plugins[*].name}' 2>/dev/null || true)
|
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
|
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)
|
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)
|
||||||
@ -343,9 +348,18 @@ wait_for_plugin_ready() {
|
|||||||
ready=${ready:-0}
|
ready=${ready:-0}
|
||||||
replicas=${replicas:-0}
|
replicas=${replicas:-0}
|
||||||
if (( ready >= 1 && replicas >= 1 )); then
|
if (( ready >= 1 && replicas >= 1 )); then
|
||||||
return 0
|
plugin_deploy_ready=1
|
||||||
|
break
|
||||||
fi
|
fi
|
||||||
done <<< "$deployment_rows"
|
done <<< "$deployment_rows"
|
||||||
|
|
||||||
|
if (( plugin_deploy_ready == 1 )); then
|
||||||
|
pod=$(kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
||||||
|
socket_path="$PLUGIN_SOCKET_DIR/$BARMAN_PLUGIN_NAME"
|
||||||
|
if [[ -n "$pod" ]] && kubectl -n "$NAMESPACE" exec "$pod" -c postgres -- test -S "$socket_path" >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
now=$(date +%s)
|
now=$(date +%s)
|
||||||
@ -391,6 +405,21 @@ wait_for_successful_base_backup() {
|
|||||||
local start_time now elapsed
|
local start_time now elapsed
|
||||||
start_time=$(date +%s)
|
start_time=$(date +%s)
|
||||||
while true; do
|
while true; do
|
||||||
|
if [[ -n "$LAST_BACKUP_NAME" ]]; then
|
||||||
|
local phase err
|
||||||
|
phase=$(kubectl -n "$NAMESPACE" get backup "$LAST_BACKUP_NAME" -o jsonpath='{.status.phase}' 2>/dev/null || true)
|
||||||
|
err=$(kubectl -n "$NAMESPACE" get backup "$LAST_BACKUP_NAME" -o jsonpath='{.status.error}' 2>/dev/null || true)
|
||||||
|
case "${phase:-}" in
|
||||||
|
Completed|Succeeded|completed|succeeded)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
Failed|failed)
|
||||||
|
echo "ERROR: Backup '$LAST_BACKUP_NAME' failed: ${err:-<no error provided>}" >&2
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
if has_successful_base_backup; then
|
if has_successful_base_backup; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@ -410,6 +439,7 @@ trigger_backup() {
|
|||||||
local backup_type="${1:-full}"
|
local backup_type="${1:-full}"
|
||||||
local backup_name
|
local backup_name
|
||||||
backup_name="${CNPG_CLUSTER_NAME}-backup-$(date +%Y%m%d%H%M%S)"
|
backup_name="${CNPG_CLUSTER_NAME}-backup-$(date +%Y%m%d%H%M%S)"
|
||||||
|
LAST_BACKUP_NAME="$backup_name"
|
||||||
echo "Triggering ${backup_type} backup $backup_name ..."
|
echo "Triggering ${backup_type} backup $backup_name ..."
|
||||||
if [[ "$backup_type" == "incremental" || "$backup_type" == "incr" ]]; then
|
if [[ "$backup_type" == "incremental" || "$backup_type" == "incr" ]]; then
|
||||||
kubectl_apply_retry "$NAMESPACE" <<BACKUP
|
kubectl_apply_retry "$NAMESPACE" <<BACKUP
|
||||||
|
|||||||
@ -25,7 +25,7 @@ usage() {
|
|||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: init_common_services.sh [-n|--namespace NS] [-k|--kerberos] <update|start|status|verify>
|
Usage: init_common_services.sh [-n|--namespace NS] [-k|--kerberos] <update|start|status|verify>
|
||||||
|
|
||||||
Deploys common infrastructure services (ArgoCD, OpenTofu, Garage, OpenBao, Kong, Cert-Manager)
|
Deploys common infrastructure services (Registry, OpenTofu, Garage, OpenBao, Kong, Cert-Manager)
|
||||||
into the given Kubernetes namespace. Use -k to include the Kerberos/KDC service.
|
into the given Kubernetes namespace. Use -k to include the Kerberos/KDC service.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@ -86,7 +86,6 @@ if [ -z "$NS" ]; then
|
|||||||
NS="default"
|
NS="default"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ARGOCD_NS="${ARGOCD_NAMESPACE:-argocd}"
|
|
||||||
# Registry should live in the common-core/service namespace unless explicitly overridden.
|
# Registry should live in the common-core/service namespace unless explicitly overridden.
|
||||||
REGISTRY_NS="${REGISTRY_NAMESPACE:-${NS}}"
|
REGISTRY_NS="${REGISTRY_NAMESPACE:-${NS}}"
|
||||||
|
|
||||||
@ -500,7 +499,7 @@ fi
|
|||||||
|
|
||||||
OPENTOFU_NAME=${OPENTOFU_NAME:-opentofu}
|
OPENTOFU_NAME=${OPENTOFU_NAME:-opentofu}
|
||||||
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
|
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
|
||||||
ARGOCD_SERVER_NAME=${ARGOCD_SERVER_NAME:-argocd-server}
|
REGISTRY_NAME=${REGISTRY_NAME:-registry}
|
||||||
GARAGE_NAME=${GARAGE_NAME:-garage}
|
GARAGE_NAME=${GARAGE_NAME:-garage}
|
||||||
KONG_NAME=${KONG_NAME:-prole-svc-kong}
|
KONG_NAME=${KONG_NAME:-prole-svc-kong}
|
||||||
OPENTOFU_CONFIGMAP=${OPENTOFU_CONFIGMAP:-opentofu-nginx}
|
OPENTOFU_CONFIGMAP=${OPENTOFU_CONFIGMAP:-opentofu-nginx}
|
||||||
@ -545,13 +544,13 @@ migrate_common_services() {
|
|||||||
kubectl delete -n "$old_ns" secret "$OPENTOFU_SECRET" --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
|
done
|
||||||
|
|
||||||
for old_ns in $(collect_other_namespaces "$ARGOCD_NS" "$ARGOCD_SERVER_NAME" deployment service); do
|
for old_ns in $(collect_other_namespaces "$REGISTRY_NS" "$REGISTRY_NAME" deployment service); do
|
||||||
echo "Found ArgoCD in namespace '$old_ns'; removing before deploy to '$ARGOCD_NS' ..."
|
echo "Found Registry ($REGISTRY_NAME) in namespace '$old_ns'; removing before deploy to '$REGISTRY_NS' ..."
|
||||||
if [ -x "$SCRIPT_DIR/init_registry.sh" ]; then
|
if [ -x "$SCRIPT_DIR/init_registry.sh" ]; then
|
||||||
REGISTRY_NAMESPACE="$REGISTRY_NS" "$SCRIPT_DIR/init_registry.sh" -n "$old_ns" stop || true
|
"$SCRIPT_DIR/init_registry.sh" -n "$old_ns" stop || true
|
||||||
else
|
else
|
||||||
kubectl delete -n "$old_ns" deploy "$ARGOCD_SERVER_NAME" --ignore-not-found >/dev/null 2>&1 || true
|
kubectl delete -n "$old_ns" deploy "$REGISTRY_NAME" --ignore-not-found >/dev/null 2>&1 || true
|
||||||
kubectl delete -n "$old_ns" svc "$ARGOCD_SERVER_NAME" --ignore-not-found >/dev/null 2>&1 || true
|
kubectl delete -n "$old_ns" svc "$REGISTRY_NAME" --ignore-not-found >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
@ -611,10 +610,10 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -x "$SCRIPT_DIR/init_registry.sh" ]; then
|
if [ -x "$SCRIPT_DIR/init_registry.sh" ]; then
|
||||||
ARGOCD_NAMESPACE="$ARGOCD_NS" REGISTRY_NAMESPACE="$REGISTRY_NS" \
|
REGISTRY_NAMESPACE="$REGISTRY_NS" SERVICE_NAMESPACE="$NS" \
|
||||||
"$SCRIPT_DIR/init_registry.sh" -n "$ARGOCD_NS" --registry-namespace "$REGISTRY_NS" "$ACTION" || rc=$?
|
"$SCRIPT_DIR/init_registry.sh" -n "$REGISTRY_NS" "$ACTION" || rc=$?
|
||||||
else
|
else
|
||||||
echo "WARN: init_registry.sh not found; ArgoCD deploy skipped."
|
echo "WARN: init_registry.sh not found; registry deploy skipped."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -x "$SCRIPT_DIR/init_garage_store.sh" ]; then
|
if [ -x "$SCRIPT_DIR/init_garage_store.sh" ]; then
|
||||||
|
|||||||
207
etc/init_gitea.sh
Normal file
207
etc/init_gitea.sh
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROG="init_gitea"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$SCRIPT_DIR/prole_cfg.sh"
|
||||||
|
|
||||||
|
MODE="$(prole_normalize_mode "${PROLE_MODE:-${DEPLOYMENT_MODE:-k3d}}")"
|
||||||
|
NAMESPACE="${GITEA_NAMESPACE:-}"
|
||||||
|
CFG_PATH=""
|
||||||
|
FORCE=0
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage:
|
||||||
|
$PROG [options] [deploy]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--mode <k3d|k3s|k8s|local> Deployment mode (default: ${MODE:-k3d})
|
||||||
|
-n, --namespace <name> Target namespace (default: gitea)
|
||||||
|
-c, --config <prole.cfg> Path to prole.cfg (defaults to detected)
|
||||||
|
--force Delete existing release before deploy
|
||||||
|
--help Show this help
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- Prefers Helm install/upgrade (gitea-charts/gitea).
|
||||||
|
- Falls back to applying a generated Deployment/Service/Ingress manifest.
|
||||||
|
- If a local registry is enabled, the image is mirrored there first.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
die() { echo "[ERROR] $*" >&2; exit 2; }
|
||||||
|
log() { echo "[INFO] $*"; }
|
||||||
|
warn() { echo "[WARN] $*" >&2; }
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--mode) MODE="$(prole_normalize_mode "${2:-}")"; shift 2 ;;
|
||||||
|
--mode=*) MODE="$(prole_normalize_mode "${1#*=}")"; shift 1 ;;
|
||||||
|
-n|--namespace) NAMESPACE="${2:-}"; shift 2 ;;
|
||||||
|
--namespace=*) NAMESPACE="${1#*=}"; shift 1 ;;
|
||||||
|
-c|--config) CFG_PATH="${2:-}"; shift 2 ;;
|
||||||
|
--config=*) CFG_PATH="${1#*=}"; shift 1 ;;
|
||||||
|
--force) FORCE=1; shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) break ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Resolve config path and namespace defaults from prole.cfg when present
|
||||||
|
if [[ -z "$CFG_PATH" && -n "${PROLE_CONF:-}" && -f "${PROLE_CONF}/prole.cfg" ]]; then
|
||||||
|
CFG_PATH="${PROLE_CONF}/prole.cfg"
|
||||||
|
elif [[ -z "$CFG_PATH" && -f "$SCRIPT_DIR/../conf/prole.cfg" ]]; then
|
||||||
|
CFG_PATH="$SCRIPT_DIR/../conf/prole.cfg"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$NAMESPACE" && -n "$CFG_PATH" ]]; then
|
||||||
|
maybe_ns="$(_prole_cfg_extract_key "$CFG_PATH" "GITOPS_NAMESPACE")"
|
||||||
|
[[ -z "$maybe_ns" ]] && maybe_ns="$(_prole_cfg_extract_key "$CFG_PATH" "GITEA_NAMESPACE")"
|
||||||
|
NAMESPACE="$maybe_ns"
|
||||||
|
fi
|
||||||
|
NAMESPACE="${NAMESPACE:-gitea}"
|
||||||
|
export GITEA_NAMESPACE="$NAMESPACE"
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
k3d|k3s|k8s|local) ;;
|
||||||
|
*) die "Unsupported mode '$MODE' (use k3d, k3s, k8s, or local)" ;;
|
||||||
|
esac
|
||||||
|
export PROLE_MODE="$MODE"
|
||||||
|
|
||||||
|
command -v kubectl >/dev/null || die "kubectl not found"
|
||||||
|
command -v helm >/dev/null || warn "helm not found — will use manifest fallback"
|
||||||
|
|
||||||
|
kubectl get ns "$NAMESPACE" >/dev/null 2>&1 || kubectl create namespace "$NAMESPACE" >/dev/null 2>&1
|
||||||
|
|
||||||
|
CHART_REPO="https://dl.gitea.com/charts"
|
||||||
|
CHART_NAME="gitea-charts/gitea"
|
||||||
|
RELEASE_NAME="gitea"
|
||||||
|
IMAGE_REPO_DEFAULT="gitea/gitea"
|
||||||
|
IMAGE_TAG="${GITEA_IMAGE_TAG:-1.22.3}"
|
||||||
|
IMAGE_REPO="$IMAGE_REPO_DEFAULT"
|
||||||
|
|
||||||
|
# Optional reset
|
||||||
|
if [[ "$FORCE" -eq 1 ]]; then
|
||||||
|
warn "--force specified; removing existing Gitea resources in namespace $NAMESPACE"
|
||||||
|
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 deploy/gitea svc/gitea-http svc/gitea-ssh ingress/gitea >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Mirror to local registry if enabled
|
||||||
|
if _prole_local_registry_enabled && command -v docker >/dev/null; then
|
||||||
|
HOST_REG="${LOCAL_REGISTRY:-${PROLE_LOCAL_REGISTRY:-}}"
|
||||||
|
INTERNAL_REG="${LOCAL_REGISTRY_INTERNAL:-${PROLE_LOCAL_REGISTRY_INTERNAL:-}}"
|
||||||
|
if [[ -n "$HOST_REG" ]]; then
|
||||||
|
HOST_IMG="${HOST_REG}/${IMAGE_REPO_DEFAULT}"
|
||||||
|
log "Pulling ${IMAGE_REPO_DEFAULT}:${IMAGE_TAG}"
|
||||||
|
docker pull "${IMAGE_REPO_DEFAULT}:${IMAGE_TAG}" >/dev/null
|
||||||
|
log "Tagging ${HOST_IMG}:${IMAGE_TAG}"
|
||||||
|
docker tag "${IMAGE_REPO_DEFAULT}:${IMAGE_TAG}" "${HOST_IMG}:${IMAGE_TAG}"
|
||||||
|
log "Pushing ${HOST_IMG}:${IMAGE_TAG}"
|
||||||
|
docker push "${HOST_IMG}:${IMAGE_TAG}" >/dev/null || warn "Push to local registry failed; continuing with upstream image"
|
||||||
|
if [[ -n "$INTERNAL_REG" ]]; then
|
||||||
|
IMAGE_REPO="${INTERNAL_REG}/gitea/gitea"
|
||||||
|
else
|
||||||
|
IMAGE_REPO="${HOST_REG}/gitea/gitea"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
deploy_with_helm() {
|
||||||
|
command -v helm >/dev/null 2>&1 || return 1
|
||||||
|
|
||||||
|
helm repo add gitea-charts "$CHART_REPO" >/dev/null 2>&1 || true
|
||||||
|
helm repo update >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Minimal config: NodePort or ClusterIP + Ingress depending on environment.
|
||||||
|
# Avoid heavy persistence defaults; users can override via values later.
|
||||||
|
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}" \
|
||||||
|
--wait --timeout 10m
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_manifest_fallback() {
|
||||||
|
warn "Helm unavailable; applying fallback manifest"
|
||||||
|
cat <<EOF | kubectl apply -n "$NAMESPACE" -f -
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: gitea
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: gitea
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: gitea
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: gitea
|
||||||
|
image: ${IMAGE_REPO}:${IMAGE_TAG}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 3000
|
||||||
|
- name: ssh
|
||||||
|
containerPort: 2222
|
||||||
|
env:
|
||||||
|
- name: GITEA__server__ROOT_URL
|
||||||
|
value: "http://gitea.${NAMESPACE}.svc.cluster.local:3000"
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: gitea-http
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: gitea
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 3000
|
||||||
|
targetPort: 3000
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: gitea-ssh
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: gitea
|
||||||
|
ports:
|
||||||
|
- name: ssh
|
||||||
|
port: 2222
|
||||||
|
targetPort: 2222
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ACTION="${1:-deploy}"
|
||||||
|
case "$ACTION" in
|
||||||
|
deploy|"") ;;
|
||||||
|
*) usage; die "Unknown action '$ACTION'" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
log "Deploying Gitea to namespace '$NAMESPACE' (mode=$MODE)"
|
||||||
|
if deploy_with_helm; then
|
||||||
|
log "Gitea deployed via Helm"
|
||||||
|
else
|
||||||
|
apply_manifest_fallback
|
||||||
|
log "Gitea deployed via manifest fallback"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Waiting for deployment rollout..."
|
||||||
|
kubectl -n "$NAMESPACE" rollout status deploy/gitea --timeout=10m
|
||||||
|
|
||||||
|
log "Done. Service endpoints:"
|
||||||
|
kubectl -n "$NAMESPACE" get svc gitea-http gitea-ssh
|
||||||
@ -57,6 +57,40 @@ OPENBAO_PORT_FORWARD_LOCAL=${OPENBAO_PORT_FORWARD_LOCAL:-8200}
|
|||||||
log() { printf '%s\n' "$*"; }
|
log() { printf '%s\n' "$*"; }
|
||||||
err() { printf '%s\n' "$*" >&2; }
|
err() { printf '%s\n' "$*" >&2; }
|
||||||
|
|
||||||
|
wait_for_ad_forwarder_ready() {
|
||||||
|
local ns="$1"
|
||||||
|
local deploy_name="$2"
|
||||||
|
local service_name="$3"
|
||||||
|
local timeout_s="${4:-120}"
|
||||||
|
|
||||||
|
log "Waiting for AD forwarder deployment '${deploy_name}' to become available in namespace '${ns}' ..."
|
||||||
|
if ! kubectl -n "$ns" rollout status deploy/"$deploy_name" --timeout="${timeout_s}s"; then
|
||||||
|
err "ERROR: AD forwarder deployment '${deploy_name}' did not become ready in namespace '${ns}'."
|
||||||
|
kubectl -n "$ns" get pods -l "app=${deploy_name}" -o wide 1>&2 || true
|
||||||
|
kubectl -n "$ns" describe deploy "${deploy_name}" 1>&2 || true
|
||||||
|
kubectl -n "$ns" logs -l "app=${deploy_name}" --tail=200 1>&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Waiting for AD forwarder service '${service_name}' endpoints in namespace '${ns}' ..."
|
||||||
|
local deadline=$((SECONDS + timeout_s))
|
||||||
|
while [[ $SECONDS -lt $deadline ]]; do
|
||||||
|
local ip
|
||||||
|
ip=$(kubectl -n "$ns" get endpoints "$service_name" -o jsonpath='{.subsets[*].addresses[*].ip}' 2>/dev/null | tr ' ' '\n' | head -n 1 || true)
|
||||||
|
if [[ -n "$ip" ]]; then
|
||||||
|
log "[OK] AD forwarder service '${service_name}' has endpoints (e.g. ${ip})."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
err "ERROR: AD forwarder service '${service_name}' has no ready endpoints in namespace '${ns}' after ${timeout_s}s."
|
||||||
|
kubectl -n "$ns" get svc "$service_name" -o wide 1>&2 || true
|
||||||
|
kubectl -n "$ns" get endpoints "$service_name" -o yaml 1>&2 || true
|
||||||
|
kubectl -n "$ns" get pods -l "app=${deploy_name}" -o wide 1>&2 || true
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
ensure_tools() {
|
ensure_tools() {
|
||||||
for t in kubectl curl jq; do
|
for t in kubectl curl jq; do
|
||||||
command -v "$t" >/dev/null || { err "Missing required tool: $t"; exit 1; }
|
command -v "$t" >/dev/null || { err "Missing required tool: $t"; exit 1; }
|
||||||
@ -817,7 +851,7 @@ $(
|
|||||||
)
|
)
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
kubectl -n "$KRB5_AD_NAMESPACE" rollout status deploy/${KRB5_AD_PROXY_NAME} --timeout=120s || true
|
wait_for_ad_forwarder_ready "$KRB5_AD_NAMESPACE" "$KRB5_AD_PROXY_NAME" "$KRB5_AD_SERVICE_NAME" 120
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup_ad_forwarder() {
|
cleanup_ad_forwarder() {
|
||||||
|
|||||||
@ -4,8 +4,9 @@ set -euo pipefail
|
|||||||
|
|
||||||
# init_registry.sh
|
# init_registry.sh
|
||||||
# Purpose:
|
# Purpose:
|
||||||
# - Deploy ArgoCD into Kubernetes
|
# - Deploy registry support for local clusters
|
||||||
# - Deploy registry:2 into k3s clusters (service namespace) when enabled
|
# - k3s: deploy in-cluster `registry:2` into the service namespace
|
||||||
|
# - k3d: create/connect a `k3d` registry on port 5000
|
||||||
|
|
||||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
|
||||||
@ -42,16 +43,6 @@ ACTION=""
|
|||||||
NAMESPACE_OVERRIDE=""
|
NAMESPACE_OVERRIDE=""
|
||||||
REGISTRY_NAMESPACE_OVERRIDE=""
|
REGISTRY_NAMESPACE_OVERRIDE=""
|
||||||
|
|
||||||
if [[ -d "$SCRIPT_DIR/../k8s/argocd" ]]; then
|
|
||||||
ARGOCD_MANIFEST_DIR="$SCRIPT_DIR/../k8s/argocd"
|
|
||||||
elif [[ -n "${PROLE_HOME:-}" && -d "$PROLE_HOME/k8s/argocd" ]]; then
|
|
||||||
ARGOCD_MANIFEST_DIR="$PROLE_HOME/k8s/argocd"
|
|
||||||
else
|
|
||||||
ARGOCD_MANIFEST_DIR="$SCRIPT_DIR/../k8s/argocd"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ARGOCD_MANIFEST_FILE=${ARGOCD_MANIFEST_FILE:-"$ARGOCD_MANIFEST_DIR/install.yaml"}
|
|
||||||
|
|
||||||
if [[ -d "$SCRIPT_DIR/../k8s/registry" ]]; then
|
if [[ -d "$SCRIPT_DIR/../k8s/registry" ]]; then
|
||||||
REGISTRY_MANIFEST_DIR="$SCRIPT_DIR/../k8s/registry"
|
REGISTRY_MANIFEST_DIR="$SCRIPT_DIR/../k8s/registry"
|
||||||
elif [[ -n "${PROLE_HOME:-}" && -d "$PROLE_HOME/k8s/registry" ]]; then
|
elif [[ -n "${PROLE_HOME:-}" && -d "$PROLE_HOME/k8s/registry" ]]; then
|
||||||
@ -66,9 +57,12 @@ usage() {
|
|||||||
cat <<EOF
|
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>
|
||||||
|
|
||||||
Deploys ArgoCD into the target namespace and, for k3s, deploys registry:2
|
Deploys registry support for local clusters.
|
||||||
into the registry namespace (default: SERVICE_NAMESPACE).
|
|
||||||
Note: The local Docker registry (port 5000) is managed separately for k3d.
|
- 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.
|
||||||
|
|
||||||
|
NOTE: `-r/--registry-namespace` is kept for backwards compatibility and is treated as an alias for `-n/--namespace`.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,16 +117,10 @@ while [[ $# -gt 0 ]]; do
|
|||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ -n "$NAMESPACE_OVERRIDE" ]]; then
|
|
||||||
ARGOCD_NAMESPACE="$NAMESPACE_OVERRIDE"
|
|
||||||
elif [[ -n "${ARGOCD_NAMESPACE:-}" ]]; then
|
|
||||||
ARGOCD_NAMESPACE="$ARGOCD_NAMESPACE"
|
|
||||||
else
|
|
||||||
ARGOCD_NAMESPACE="argocd"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$REGISTRY_NAMESPACE_OVERRIDE" ]]; then
|
if [[ -n "$REGISTRY_NAMESPACE_OVERRIDE" ]]; then
|
||||||
REGISTRY_NAMESPACE="$REGISTRY_NAMESPACE_OVERRIDE"
|
REGISTRY_NAMESPACE="$REGISTRY_NAMESPACE_OVERRIDE"
|
||||||
|
elif [[ -n "$NAMESPACE_OVERRIDE" ]]; then
|
||||||
|
REGISTRY_NAMESPACE="$NAMESPACE_OVERRIDE"
|
||||||
elif [[ -n "${REGISTRY_NAMESPACE:-}" ]]; then
|
elif [[ -n "${REGISTRY_NAMESPACE:-}" ]]; then
|
||||||
REGISTRY_NAMESPACE="$REGISTRY_NAMESPACE"
|
REGISTRY_NAMESPACE="$REGISTRY_NAMESPACE"
|
||||||
else
|
else
|
||||||
@ -151,15 +139,7 @@ if [[ "$_mode_resolved" == "k3s" && ( -z "${REGISTRY_NAMESPACE:-}" || "${REGISTR
|
|||||||
fi
|
fi
|
||||||
unset _mode_resolved
|
unset _mode_resolved
|
||||||
|
|
||||||
export ARGOCD_NAMESPACE
|
|
||||||
export REGISTRY_NAMESPACE
|
export REGISTRY_NAMESPACE
|
||||||
export NAMESPACE="$ARGOCD_NAMESPACE"
|
|
||||||
|
|
||||||
ARGOCD_LABEL_SELECTOR=${ARGOCD_LABEL_SELECTOR:-"app.kubernetes.io/part-of=argocd"}
|
|
||||||
ARGOCD_SERVER_SERVICE=${ARGOCD_SERVER_SERVICE:-argocd-server}
|
|
||||||
ARGOCD_PORT_FORWARD_LOCAL=${ARGOCD_PORT_FORWARD_LOCAL:-8081}
|
|
||||||
ARGOCD_PORT_FORWARD_REMOTE=${ARGOCD_PORT_FORWARD_REMOTE:-80}
|
|
||||||
ARGOCD_NODE_SELECTOR=${ARGOCD_NODE_SELECTOR:-}
|
|
||||||
REGISTRY_NODE_SELECTOR=${REGISTRY_NODE_SELECTOR:-}
|
REGISTRY_NODE_SELECTOR=${REGISTRY_NODE_SELECTOR:-}
|
||||||
|
|
||||||
current_mode() {
|
current_mode() {
|
||||||
@ -176,11 +156,7 @@ registry_enabled() {
|
|||||||
[[ "$mode" == "k3s" || "$mode" == "k3d" ]]
|
[[ "$mode" == "k3s" || "$mode" == "k3d" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve_argocd_node_selector() {
|
resolve_control_plane_node_selector() {
|
||||||
if [[ -n "$ARGOCD_NODE_SELECTOR" ]]; then
|
|
||||||
printf '%s' "$ARGOCD_NODE_SELECTOR"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
local node=""
|
local node=""
|
||||||
node=$(kubectl get nodes -l 'node-role.kubernetes.io/control-plane' -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
node=$(kubectl get nodes -l 'node-role.kubernetes.io/control-plane' -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
||||||
if [[ -n "$node" ]]; then
|
if [[ -n "$node" ]]; then
|
||||||
@ -195,7 +171,7 @@ resolve_registry_node_selector() {
|
|||||||
fi
|
fi
|
||||||
# Default: pin to the control-plane node so hostPort:5000 is reachable via
|
# Default: pin to the control-plane node so hostPort:5000 is reachable via
|
||||||
# the k3s server host (e.g. myrddin.prole.org:5000).
|
# the k3s server host (e.g. myrddin.prole.org:5000).
|
||||||
resolve_argocd_node_selector
|
resolve_control_plane_node_selector
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_registry_node_selector() {
|
apply_registry_node_selector() {
|
||||||
@ -215,32 +191,6 @@ apply_registry_node_selector() {
|
|||||||
-p "{\"spec\":{\"template\":{\"spec\":{\"nodeSelector\":{\"$key\":\"$val\"}}}}}" >/dev/null 2>&1 || true
|
-p "{\"spec\":{\"template\":{\"spec\":{\"nodeSelector\":{\"$key\":\"$val\"}}}}}" >/dev/null 2>&1 || true
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_argocd_node_selector() {
|
|
||||||
local selector
|
|
||||||
selector="$(resolve_argocd_node_selector)"
|
|
||||||
if [[ -z "$selector" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
local key="${selector%%=*}"
|
|
||||||
local val="${selector#*=}"
|
|
||||||
if [[ -z "$key" || -z "$val" ]]; then
|
|
||||||
echo "WARN: invalid ARGOCD_NODE_SELECTOR '$selector' (expected key=value); skipping." >&2
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
for target in \
|
|
||||||
deployment/argocd-applicationset-controller \
|
|
||||||
deployment/argocd-dex-server \
|
|
||||||
deployment/argocd-notifications-controller \
|
|
||||||
deployment/argocd-repo-server \
|
|
||||||
deployment/argocd-server \
|
|
||||||
deployment/argocd-redis \
|
|
||||||
statefulset/argocd-application-controller; do
|
|
||||||
kubectl -n "$ARGOCD_NAMESPACE" patch "$target" \
|
|
||||||
--type merge \
|
|
||||||
-p "{\"spec\":{\"template\":{\"spec\":{\"nodeSelector\":{\"$key\":\"$val\"}}}}}" >/dev/null 2>&1 || true
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_tools() {
|
ensure_tools() {
|
||||||
command -v kubectl >/dev/null || { echo "Missing required tool: kubectl" >&2; exit 1; }
|
command -v kubectl >/dev/null || { echo "Missing required tool: kubectl" >&2; exit 1; }
|
||||||
}
|
}
|
||||||
@ -256,14 +206,6 @@ ensure_namespace() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
render_manifest() {
|
|
||||||
if [[ ! -f "$ARGOCD_MANIFEST_FILE" ]]; then
|
|
||||||
echo "ERROR: ArgoCD manifest not found: $ARGOCD_MANIFEST_FILE" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sed "s|\\${ARGOCD_NAMESPACE}|$ARGOCD_NAMESPACE|g" "$ARGOCD_MANIFEST_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
render_registry_manifest() {
|
render_registry_manifest() {
|
||||||
if [[ ! -f "$REGISTRY_MANIFEST_FILE" ]]; then
|
if [[ ! -f "$REGISTRY_MANIFEST_FILE" ]]; then
|
||||||
echo "ERROR: Registry manifest not found: $REGISTRY_MANIFEST_FILE" >&2
|
echo "ERROR: Registry manifest not found: $REGISTRY_MANIFEST_FILE" >&2
|
||||||
@ -276,19 +218,6 @@ cleanup_registry_k8s() {
|
|||||||
render_registry_manifest | kubectl delete -n "$REGISTRY_NAMESPACE" -f - --ignore-not-found >/dev/null 2>&1 || true
|
render_registry_manifest | kubectl delete -n "$REGISTRY_NAMESPACE" -f - --ignore-not-found >/dev/null 2>&1 || true
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_k8s() {
|
|
||||||
echo "Applying ArgoCD manifest to namespace '$ARGOCD_NAMESPACE' ..."
|
|
||||||
render_manifest | kubectl apply --server-side --force-conflicts --field-manager=prole-installer --validate=false -n "$ARGOCD_NAMESPACE" -f -
|
|
||||||
apply_argocd_node_selector
|
|
||||||
kubectl rollout status deploy/argocd-server -n "$ARGOCD_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
|
|
||||||
kubectl rollout status deploy/argocd-repo-server -n "$ARGOCD_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
|
|
||||||
kubectl rollout status deploy/argocd-dex-server -n "$ARGOCD_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
|
|
||||||
kubectl rollout status deploy/argocd-applicationset-controller -n "$ARGOCD_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
|
|
||||||
kubectl rollout status deploy/argocd-notifications-controller -n "$ARGOCD_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
|
|
||||||
kubectl rollout status deploy/argocd-redis -n "$ARGOCD_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
|
|
||||||
kubectl rollout status statefulset/argocd-application-controller -n "$ARGOCD_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
|
|
||||||
}
|
|
||||||
|
|
||||||
configure_k3d_insecure_registry() {
|
configure_k3d_insecure_registry() {
|
||||||
local cluster_name="${1:-knoe-dev-cluster}"
|
local cluster_name="${1:-knoe-dev-cluster}"
|
||||||
local reg_host="k3d-prole-registry"
|
local reg_host="k3d-prole-registry"
|
||||||
@ -454,11 +383,6 @@ apply_registry() {
|
|||||||
kubectl rollout status deploy/registry -n "$REGISTRY_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
|
kubectl rollout status deploy/registry -n "$REGISTRY_NAMESPACE" --timeout=${ROLLOUT_TIMEOUT:-300s} || true
|
||||||
}
|
}
|
||||||
|
|
||||||
delete_k8s() {
|
|
||||||
echo "Removing ArgoCD resources from namespace '$ARGOCD_NAMESPACE' ..."
|
|
||||||
render_manifest | kubectl delete -n "$ARGOCD_NAMESPACE" -f - --ignore-not-found
|
|
||||||
}
|
|
||||||
|
|
||||||
delete_registry() {
|
delete_registry() {
|
||||||
local mode
|
local mode
|
||||||
mode=$(current_mode)
|
mode=$(current_mode)
|
||||||
@ -475,12 +399,18 @@ delete_registry() {
|
|||||||
render_registry_manifest | kubectl delete -n "$REGISTRY_NAMESPACE" -f - --ignore-not-found
|
render_registry_manifest | kubectl delete -n "$REGISTRY_NAMESPACE" -f - --ignore-not-found
|
||||||
}
|
}
|
||||||
|
|
||||||
status_k8s() {
|
|
||||||
kubectl -n "$ARGOCD_NAMESPACE" get deploy,statefulset,svc -l "$ARGOCD_LABEL_SELECTOR" 2>/dev/null || true
|
|
||||||
kubectl -n "$ARGOCD_NAMESPACE" get pods -l "$ARGOCD_LABEL_SELECTOR" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
status_registry() {
|
status_registry() {
|
||||||
|
local mode
|
||||||
|
mode=$(current_mode)
|
||||||
|
if [[ "$mode" == "k3d" ]]; then
|
||||||
|
if command -v k3d >/dev/null 2>&1; then
|
||||||
|
k3d registry list 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if command -v docker >/dev/null 2>&1; then
|
||||||
|
docker ps --format '{{.Names}}\t{{.Image}}\t{{.Ports}}' 2>/dev/null | grep -E 'k3d-prole-registry|prole-registry' || true
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
kubectl -n "$REGISTRY_NAMESPACE" get deploy,svc -l "app=registry" 2>/dev/null || true
|
kubectl -n "$REGISTRY_NAMESPACE" get deploy,svc -l "app=registry" 2>/dev/null || true
|
||||||
kubectl -n "$REGISTRY_NAMESPACE" get pods -l "app=registry" 2>/dev/null || true
|
kubectl -n "$REGISTRY_NAMESPACE" get pods -l "app=registry" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
@ -488,27 +418,21 @@ status_registry() {
|
|||||||
case "${ACTION:-}" in
|
case "${ACTION:-}" in
|
||||||
start|initialize|update|reload|restart)
|
start|initialize|update|reload|restart)
|
||||||
ensure_tools
|
ensure_tools
|
||||||
ensure_namespace "$ARGOCD_NAMESPACE"
|
|
||||||
apply_k8s
|
|
||||||
if registry_enabled; then
|
if registry_enabled; then
|
||||||
ensure_namespace "$REGISTRY_NAMESPACE"
|
ensure_namespace "$REGISTRY_NAMESPACE"
|
||||||
apply_registry
|
apply_registry
|
||||||
else
|
else
|
||||||
echo "Skipping registry deploy (mode=$(current_mode))"
|
echo "Skipping registry deploy (mode=$(current_mode))"
|
||||||
fi
|
fi
|
||||||
prole_register_port_forward "argocd" "$ARGOCD_NAMESPACE" "svc/${ARGOCD_SERVER_SERVICE}" \
|
|
||||||
"$ARGOCD_PORT_FORWARD_LOCAL" "$ARGOCD_PORT_FORWARD_REMOTE" "0.0.0.0" "TCP" "ArgoCD"
|
|
||||||
;;
|
;;
|
||||||
stop)
|
stop)
|
||||||
ensure_tools
|
ensure_tools
|
||||||
delete_k8s
|
|
||||||
if registry_enabled; then
|
if registry_enabled; then
|
||||||
delete_registry
|
delete_registry
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
status)
|
status)
|
||||||
ensure_tools
|
ensure_tools
|
||||||
status_k8s
|
|
||||||
if registry_enabled; then
|
if registry_enabled; then
|
||||||
status_registry
|
status_registry
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -4,7 +4,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
# init_service_layer.sh
|
# init_service_layer.sh
|
||||||
# Purpose:
|
# Purpose:
|
||||||
# - Deploy the Prole service layer (ArgoCD, OpenTofu, Garage, OpenBao; Kerberos optional)
|
# - Deploy the Prole service layer (OpenTofu, Garage, OpenBao, Kong; Kerberos optional)
|
||||||
# - Keep service-layer resources grouped in SERVICE_NAMESPACE
|
# - Keep service-layer resources grouped in SERVICE_NAMESPACE
|
||||||
# - Migrate service layer to a new namespace
|
# - Migrate service layer to a new namespace
|
||||||
|
|
||||||
@ -36,10 +36,11 @@ SERVICE_NAMESPACE_OVERRIDE=""
|
|||||||
FROM_NAMESPACE=""
|
FROM_NAMESPACE=""
|
||||||
TO_NAMESPACE=""
|
TO_NAMESPACE=""
|
||||||
ENABLE_KERBEROS=0
|
ENABLE_KERBEROS=0
|
||||||
|
ENABLE_ARGOCD=0
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: init_service_layer.sh [-n|--namespace NS] [-k|--kerberos] [--from OLD_NS] [--to NEW_NS] <start|update|restart|status|stop|migrate>
|
Usage: init_service_layer.sh [-n|--namespace NS] [-k|--kerberos] [-a|--argocd] [--from OLD_NS] [--to NEW_NS] <start|update|restart|status|stop|migrate>
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,6 +72,10 @@ while [[ $# -gt 0 ]]; do
|
|||||||
ENABLE_KERBEROS=1
|
ENABLE_KERBEROS=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
-a|--argocd)
|
||||||
|
ENABLE_ARGOCD=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
--from)
|
--from)
|
||||||
shift
|
shift
|
||||||
FROM_NAMESPACE="${1:-}"
|
FROM_NAMESPACE="${1:-}"
|
||||||
@ -215,8 +220,10 @@ deploy_service_layer() {
|
|||||||
*) kdc_action="update" ;;
|
*) kdc_action="update" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
ARGOCD_NAMESPACE="$ARGOCD_NS" REGISTRY_NAMESPACE="$REGISTRY_NS" SERVICE_NAMESPACE="$ns" \
|
if [[ "$ENABLE_ARGOCD" == "1" ]]; then
|
||||||
"$SCRIPT_DIR/init_registry.sh" -n "$ARGOCD_NS" --registry-namespace "$REGISTRY_NS" "$argocd_action" || rc=$?
|
ARGOCD_NAMESPACE="$ARGOCD_NS" SERVICE_NAMESPACE="$ns" \
|
||||||
|
"$SCRIPT_DIR/init_argocd.sh" -n "$ARGOCD_NS" "$argocd_action" || rc=$?
|
||||||
|
fi
|
||||||
|
|
||||||
OPENTOFU_NAMESPACE="$ns" OPENTOFU_SECRET_NAMESPACE="${NAMESPACE:-$ns}" OPENTOFU_OPENBAO_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
|
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=$?
|
"$SCRIPT_DIR/init_opentofu.sh" -n "$ns" "$opentofu_action" || rc=$?
|
||||||
@ -244,8 +251,10 @@ deploy_service_layer() {
|
|||||||
cleanup_old_namespace() {
|
cleanup_old_namespace() {
|
||||||
local ns="$1"
|
local ns="$1"
|
||||||
log "Cleaning up service layer in old namespace '$ns' ..."
|
log "Cleaning up service layer in old namespace '$ns' ..."
|
||||||
ARGOCD_NAMESPACE="$ARGOCD_NS" REGISTRY_NAMESPACE="$REGISTRY_NS" SERVICE_NAMESPACE="$ns" \
|
if [[ "$ENABLE_ARGOCD" == "1" ]]; then
|
||||||
"$SCRIPT_DIR/init_registry.sh" -n "$ARGOCD_NS" --registry-namespace "$REGISTRY_NS" stop || true
|
ARGOCD_NAMESPACE="$ARGOCD_NS" SERVICE_NAMESPACE="$ns" \
|
||||||
|
"$SCRIPT_DIR/init_argocd.sh" -n "$ARGOCD_NS" stop || true
|
||||||
|
fi
|
||||||
OPENTOFU_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
|
OPENTOFU_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
|
||||||
"$SCRIPT_DIR/init_opentofu.sh" -n "$ns" stop || true
|
"$SCRIPT_DIR/init_opentofu.sh" -n "$ns" stop || true
|
||||||
OPENBAO_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
|
OPENBAO_NAMESPACE="$ns" SERVICE_NAMESPACE="$ns" \
|
||||||
|
|||||||
@ -925,16 +925,61 @@ prole_ensure_kube_context() {
|
|||||||
current_context=$(kubectl config current-context 2>/dev/null || true)
|
current_context=$(kubectl config current-context 2>/dev/null || true)
|
||||||
|
|
||||||
if [[ -n "${KUBECONTEXT:-}" && "$current_context" != "$KUBECONTEXT" ]]; then
|
if [[ -n "${KUBECONTEXT:-}" && "$current_context" != "$KUBECONTEXT" ]]; then
|
||||||
echo "DEBUG: prole_ensure_kube_context: Switching context to '${KUBECONTEXT}'" >&2
|
local desired_context
|
||||||
echo "INFO: Switching kubectl context to '${KUBECONTEXT}' (prole.cfg overrides local env)" >&2
|
desired_context="$KUBECONTEXT"
|
||||||
if kubectl config use-context "$KUBECONTEXT" >/dev/null 2>&1; then
|
|
||||||
echo "INFO: kubectl context set to '${KUBECONTEXT}'" >&2
|
local context_names
|
||||||
|
context_names=$(kubectl config get-contexts -o name 2>/dev/null || true)
|
||||||
|
|
||||||
|
local do_switch
|
||||||
|
do_switch=1
|
||||||
|
|
||||||
|
# If the requested context isn't present, try to map common shorthand to real contexts.
|
||||||
|
# For k3d, contexts are typically named `k3d-<cluster_name>`.
|
||||||
|
if ! grep -Fxq "$desired_context" <<<"$context_names"; then
|
||||||
|
if [[ "$mode" == "k3d" && "$desired_context" != k3d-* ]]; then
|
||||||
|
local prefixed
|
||||||
|
prefixed="k3d-${desired_context}"
|
||||||
|
if grep -Fxq "$prefixed" <<<"$context_names"; then
|
||||||
|
desired_context="$prefixed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# k3s-generated kubeconfigs often contain a single context called `default`.
|
||||||
|
# If the config only has one context, use it even when the configured KUBECONTEXT name
|
||||||
|
# doesn't match.
|
||||||
|
if ! grep -Fxq "$desired_context" <<<"$context_names"; then
|
||||||
|
local only_context=""
|
||||||
|
local context_count=0
|
||||||
|
while IFS= read -r _ctx; do
|
||||||
|
[[ -z "${_ctx:-}" ]] && continue
|
||||||
|
context_count=$((context_count + 1))
|
||||||
|
only_context="$_ctx"
|
||||||
|
done <<<"$context_names"
|
||||||
|
unset _ctx
|
||||||
|
|
||||||
|
if [[ $context_count -eq 1 && -n "${only_context:-}" ]]; then
|
||||||
|
echo "WARN: Requested kubectl context '${desired_context}' not found in active KUBECONFIG; using only available context '${only_context}'" >&2
|
||||||
|
desired_context="$only_context"
|
||||||
|
else
|
||||||
|
echo "WARN: Requested kubectl context '${desired_context}' not found in active KUBECONFIG; skipping explicit context switch and falling back to mode-based selection" >&2
|
||||||
|
do_switch=0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $do_switch -eq 1 ]]; then
|
||||||
|
echo "DEBUG: prole_ensure_kube_context: Switching context to '${desired_context}'" >&2
|
||||||
|
echo "INFO: Switching kubectl context to '${desired_context}' (prole.cfg overrides local env)" >&2
|
||||||
|
if kubectl config use-context "$desired_context" >/dev/null 2>&1; then
|
||||||
|
echo "INFO: kubectl context set to '${desired_context}'" >&2
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
echo "ERROR: Could not switch to context '${KUBECONTEXT}'. Available contexts:" >&2
|
echo "ERROR: Could not switch to context '${desired_context}'. Available contexts:" >&2
|
||||||
kubectl config get-contexts --no-headers 2>/dev/null | awk '{print " "$2}' >&2 || true
|
kubectl config get-contexts --no-headers 2>/dev/null | awk '{print " "$2}' >&2 || true
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "DEBUG: prole_ensure_kube_context: current_context='${current_context}', KUBECONTEXT='${KUBECONTEXT:-<not-set>}'" >&2
|
echo "DEBUG: prole_ensure_kube_context: current_context='${current_context}', KUBECONTEXT='${KUBECONTEXT:-<not-set>}'" >&2
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -118,6 +118,8 @@ if [[ -z "$PROLE_NAMESPACE" ]]; then
|
|||||||
PROLE_NAMESPACE="default"
|
PROLE_NAMESPACE="default"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
_deployment_mode="$(_read_cfg_value 'DEPLOYMENT_MODE')"
|
||||||
|
|
||||||
if [[ -z "$KUBECONFIG" ]]; then
|
if [[ -z "$KUBECONFIG" ]]; then
|
||||||
_kc="$(_read_cfg_value 'KUBECONFIG')"
|
_kc="$(_read_cfg_value 'KUBECONFIG')"
|
||||||
if [[ -n "$_kc" && -f "$_kc" ]]; then
|
if [[ -n "$_kc" && -f "$_kc" ]]; then
|
||||||
@ -126,9 +128,13 @@ if [[ -z "$KUBECONFIG" ]]; then
|
|||||||
fi
|
fi
|
||||||
# Fallback: project-local kubeconfig (only if it can reach the cluster)
|
# Fallback: project-local kubeconfig (only if it can reach the cluster)
|
||||||
if [[ -z "${KUBECONFIG:-}" && -f "$PROJECT_ROOT/prole-k3s.kubeconfig" ]]; then
|
if [[ -z "${KUBECONFIG:-}" && -f "$PROJECT_ROOT/prole-k3s.kubeconfig" ]]; then
|
||||||
|
# Only auto-select the project-local k3s kubeconfig when explicitly running in k3s mode.
|
||||||
|
# In k3d mode, this kubeconfig may be reachable but is the wrong cluster.
|
||||||
|
if [[ "${_deployment_mode,,}" == "k3s" ]]; then
|
||||||
if KUBECONFIG="$PROJECT_ROOT/prole-k3s.kubeconfig" kubectl cluster-info >/dev/null 2>&1; then
|
if KUBECONFIG="$PROJECT_ROOT/prole-k3s.kubeconfig" kubectl cluster-info >/dev/null 2>&1; then
|
||||||
export KUBECONFIG="$PROJECT_ROOT/prole-k3s.kubeconfig"
|
export KUBECONFIG="$PROJECT_ROOT/prole-k3s.kubeconfig"
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_kerberos_enabled="$(_read_cfg_value 'KERBEROS_ENABLED')"
|
_kerberos_enabled="$(_read_cfg_value 'KERBEROS_ENABLED')"
|
||||||
@ -360,7 +366,11 @@ if [[ -f "$SCRIPT_DIR/status_common_services.sh" ]]; then
|
|||||||
echo ""
|
echo ""
|
||||||
printf "${BOLD}Common Services${NC}\n"
|
printf "${BOLD}Common Services${NC}\n"
|
||||||
echo "───────────────────────────────────────────"
|
echo "───────────────────────────────────────────"
|
||||||
cs_args=("-n" "$NAMESPACE")
|
_service_ns="$(_read_cfg_value 'SERVICE_NAMESPACE')"
|
||||||
|
if [[ -z "${_service_ns:-}" ]]; then
|
||||||
|
_service_ns="$PROLE_NAMESPACE"
|
||||||
|
fi
|
||||||
|
cs_args=("-n" "${_service_ns:-default}")
|
||||||
if _is_true "${_kerberos_enabled:-false}"; then
|
if _is_true "${_kerberos_enabled:-false}"; then
|
||||||
cs_args+=("-k")
|
cs_args+=("-k")
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -25,7 +25,7 @@ usage() {
|
|||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: status_common_services.sh [-n|--namespace NS] [-k|--kerberos]
|
Usage: status_common_services.sh [-n|--namespace NS] [-k|--kerberos]
|
||||||
|
|
||||||
Checks common infrastructure services (ArgoCD, OpenTofu, Garage, OpenBao).
|
Checks common infrastructure services (Registry, OpenTofu, Garage, OpenBao, Kong, Cert-Manager).
|
||||||
Use -k to include the Prole KDC (auth) checks.
|
Use -k to include the Prole KDC (auth) checks.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@ -78,7 +78,7 @@ if [ -z "$NS" ]; then
|
|||||||
NS="default"
|
NS="default"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ARGOCD_NS="${ARGOCD_NAMESPACE:-argocd}"
|
REGISTRY_NS="${REGISTRY_NAMESPACE:-${NS}}"
|
||||||
KONG_NS="${KONG_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-}}}"
|
KONG_NS="${KONG_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-}}}"
|
||||||
CERTMGR_NS="${CERTMGR_NAMESPACE:-cert-manager}"
|
CERTMGR_NS="${CERTMGR_NAMESPACE:-cert-manager}"
|
||||||
KONG_NAME="${KONG_NAME:-prole-svc-kong}"
|
KONG_NAME="${KONG_NAME:-prole-svc-kong}"
|
||||||
@ -95,6 +95,20 @@ if ! command -v kubectl >/dev/null 2>&1; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
current_mode() {
|
||||||
|
if declare -F prole_normalize_mode >/dev/null 2>&1; then
|
||||||
|
prole_normalize_mode "${PROLE_MODE:-${DEPLOYMENT_MODE:-}}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf '%s' "${PROLE_MODE:-${DEPLOYMENT_MODE:-}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
MODE="$(current_mode)"
|
||||||
|
REGISTRY_CHECK=0
|
||||||
|
if [[ "$MODE" == "k3s" || "$MODE" == "k3d" ]]; then
|
||||||
|
REGISTRY_CHECK=1
|
||||||
|
fi
|
||||||
|
|
||||||
timestamp=$(date "+%Y-%m-%d %H:%M:%S")
|
timestamp=$(date "+%Y-%m-%d %H:%M:%S")
|
||||||
ctx=$(kubectl config current-context 2>/dev/null || true)
|
ctx=$(kubectl config current-context 2>/dev/null || true)
|
||||||
server=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}' 2>/dev/null || true)
|
server=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}' 2>/dev/null || true)
|
||||||
@ -103,11 +117,12 @@ echo "Common service status"
|
|||||||
echo "Time: $timestamp"
|
echo "Time: $timestamp"
|
||||||
echo "Context: ${ctx:-<unknown>}"
|
echo "Context: ${ctx:-<unknown>}"
|
||||||
echo "Server: ${server:-<unknown>}"
|
echo "Server: ${server:-<unknown>}"
|
||||||
|
echo "Mode: ${MODE:-<unknown>}"
|
||||||
if [[ -n "${KUBECONFIG:-}" ]]; then
|
if [[ -n "${KUBECONFIG:-}" ]]; then
|
||||||
echo "Kubeconfig: $KUBECONFIG"
|
echo "Kubeconfig: $KUBECONFIG"
|
||||||
fi
|
fi
|
||||||
echo "Namespace: $NS"
|
echo "Namespace: $NS"
|
||||||
echo "ArgoCD Namespace: $ARGOCD_NS"
|
echo "Registry Namespace: $REGISTRY_NS"
|
||||||
echo "Kong Namespace: $KONG_NS"
|
echo "Kong Namespace: $KONG_NS"
|
||||||
echo "Cert-Manager Namespace: $CERTMGR_NS"
|
echo "Cert-Manager Namespace: $CERTMGR_NS"
|
||||||
echo ""
|
echo ""
|
||||||
@ -119,7 +134,11 @@ run_cmd() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
echo "== Services =="
|
echo "== Services =="
|
||||||
run_cmd kubectl -n "$ARGOCD_NS" get svc argocd-server
|
if [[ "$REGISTRY_CHECK" -eq 1 && "$MODE" != "k3d" ]]; then
|
||||||
|
run_cmd kubectl -n "$REGISTRY_NS" get svc registry
|
||||||
|
elif [[ "$REGISTRY_CHECK" -eq 1 && "$MODE" == "k3d" ]]; then
|
||||||
|
run_cmd k3d registry list
|
||||||
|
fi
|
||||||
run_cmd kubectl -n "$NS" get svc opentofu garage openbao
|
run_cmd kubectl -n "$NS" get svc opentofu garage openbao
|
||||||
if [[ "$ENABLE_KERBEROS" == "1" ]]; then
|
if [[ "$ENABLE_KERBEROS" == "1" ]]; then
|
||||||
run_cmd kubectl -n "$NS" get svc auth
|
run_cmd kubectl -n "$NS" get svc auth
|
||||||
@ -128,8 +147,9 @@ run_cmd kubectl -n "$KONG_NS" get svc "$KONG_NAME"
|
|||||||
run_cmd kubectl -n "$CERTMGR_NS" get svc cert-manager cert-manager-webhook
|
run_cmd kubectl -n "$CERTMGR_NS" get svc cert-manager cert-manager-webhook
|
||||||
|
|
||||||
echo "== Workloads =="
|
echo "== Workloads =="
|
||||||
run_cmd kubectl -n "$ARGOCD_NS" get deploy argocd-server argocd-repo-server argocd-dex-server argocd-applicationset-controller argocd-notifications-controller argocd-redis
|
if [[ "$REGISTRY_CHECK" -eq 1 && "$MODE" != "k3d" ]]; then
|
||||||
run_cmd kubectl -n "$ARGOCD_NS" get statefulset argocd-application-controller
|
run_cmd kubectl -n "$REGISTRY_NS" get deploy registry
|
||||||
|
fi
|
||||||
run_cmd kubectl -n "$NS" get deploy opentofu
|
run_cmd kubectl -n "$NS" get deploy opentofu
|
||||||
if kubectl -n "$NS" get statefulset openbao >/dev/null 2>&1; then
|
if kubectl -n "$NS" get statefulset openbao >/dev/null 2>&1; then
|
||||||
run_cmd kubectl -n "$NS" get statefulset openbao
|
run_cmd kubectl -n "$NS" get statefulset openbao
|
||||||
@ -144,7 +164,9 @@ run_cmd kubectl -n "$KONG_NS" get deploy "$KONG_NAME"
|
|||||||
run_cmd kubectl -n "$CERTMGR_NS" get deploy cert-manager cert-manager-cainjector cert-manager-webhook
|
run_cmd kubectl -n "$CERTMGR_NS" get deploy cert-manager cert-manager-cainjector cert-manager-webhook
|
||||||
|
|
||||||
echo "== Pods =="
|
echo "== Pods =="
|
||||||
run_cmd kubectl -n "$ARGOCD_NS" get pods | grep -Ei "argocd" || true
|
if [[ "$REGISTRY_CHECK" -eq 1 && "$MODE" != "k3d" ]]; then
|
||||||
|
run_cmd kubectl -n "$REGISTRY_NS" get pods | grep -Ei "registry" || true
|
||||||
|
fi
|
||||||
run_cmd kubectl -n "$NS" get pods | grep -Ei "opentofu|garage|openbao" || true
|
run_cmd kubectl -n "$NS" get pods | grep -Ei "opentofu|garage|openbao" || true
|
||||||
if [[ "$ENABLE_KERBEROS" == "1" ]]; then
|
if [[ "$ENABLE_KERBEROS" == "1" ]]; then
|
||||||
run_cmd kubectl -n "$NS" get pods | grep -Ei "auth" || true
|
run_cmd kubectl -n "$NS" get pods | grep -Ei "auth" || true
|
||||||
@ -224,7 +246,52 @@ check_resource() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
check_resource svc argocd-server "$ARGOCD_NS"
|
check_deploy_image() {
|
||||||
|
local name="$1"
|
||||||
|
local ns="$2"
|
||||||
|
local expected="$3"
|
||||||
|
local actual
|
||||||
|
actual=$(kubectl -n "$ns" get deploy "$name" -o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null || true)
|
||||||
|
if [[ -z "${actual:-}" ]]; then
|
||||||
|
echo "[FAIL] image/$name is missing (ns=$ns)"
|
||||||
|
missing=$((missing + 1))
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "$actual" != "$expected" ]]; then
|
||||||
|
echo "[FAIL] image/$name expected '$expected' got '$actual' (ns=$ns)"
|
||||||
|
missing=$((missing + 1))
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "[OK] image/$name is '$expected' (ns=$ns)"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_k3d_registry() {
|
||||||
|
if command -v k3d >/dev/null 2>&1; then
|
||||||
|
if k3d registry list --no-headers 2>/dev/null | awk '{print $1}' | grep -qx 'prole-registry'; then
|
||||||
|
echo "[OK] registry/registry exists (k3d: prole-registry)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if command -v docker >/dev/null 2>&1; then
|
||||||
|
if docker ps --format '{{.Names}}' 2>/dev/null | grep -Eq '^(k3d-prole-registry|prole-registry)$'; then
|
||||||
|
echo "[OK] registry/registry exists (docker)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "[FAIL] registry/registry is missing (k3d/docker)"
|
||||||
|
missing=$((missing + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$REGISTRY_CHECK" -eq 1 ]]; then
|
||||||
|
if [[ "$MODE" == "k3d" ]]; then
|
||||||
|
check_k3d_registry
|
||||||
|
else
|
||||||
|
check_resource svc registry "$REGISTRY_NS"
|
||||||
|
check_resource deploy registry "$REGISTRY_NS"
|
||||||
|
check_deploy_image registry "$REGISTRY_NS" 'registry:2'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
check_resource svc opentofu "$NS"
|
check_resource svc opentofu "$NS"
|
||||||
check_resource svc garage "$NS"
|
check_resource svc garage "$NS"
|
||||||
check_resource svc openbao "$NS"
|
check_resource svc openbao "$NS"
|
||||||
@ -235,12 +302,6 @@ check_resource svc "$KONG_NAME" "$KONG_NS"
|
|||||||
check_resource svc cert-manager "$CERTMGR_NS"
|
check_resource svc cert-manager "$CERTMGR_NS"
|
||||||
check_resource svc cert-manager-webhook "$CERTMGR_NS"
|
check_resource svc cert-manager-webhook "$CERTMGR_NS"
|
||||||
|
|
||||||
check_resource deploy argocd-server "$ARGOCD_NS"
|
|
||||||
check_resource deploy argocd-repo-server "$ARGOCD_NS"
|
|
||||||
check_resource deploy argocd-dex-server "$ARGOCD_NS"
|
|
||||||
check_resource deploy argocd-applicationset-controller "$ARGOCD_NS"
|
|
||||||
check_resource deploy argocd-notifications-controller "$ARGOCD_NS"
|
|
||||||
check_resource deploy argocd-redis "$ARGOCD_NS"
|
|
||||||
check_resource deploy opentofu "$NS"
|
check_resource deploy opentofu "$NS"
|
||||||
if kubectl -n "$NS" get statefulset openbao >/dev/null 2>&1; then
|
if kubectl -n "$NS" get statefulset openbao >/dev/null 2>&1; then
|
||||||
check_resource statefulset openbao "$NS"
|
check_resource statefulset openbao "$NS"
|
||||||
@ -250,7 +311,6 @@ fi
|
|||||||
if [[ "$ENABLE_KERBEROS" == "1" ]]; then
|
if [[ "$ENABLE_KERBEROS" == "1" ]]; then
|
||||||
check_resource deploy auth "$NS"
|
check_resource deploy auth "$NS"
|
||||||
fi
|
fi
|
||||||
check_resource statefulset argocd-application-controller "$ARGOCD_NS"
|
|
||||||
check_resource statefulset garage "$NS"
|
check_resource statefulset garage "$NS"
|
||||||
check_resource deploy "$KONG_NAME" "$KONG_NS"
|
check_resource deploy "$KONG_NAME" "$KONG_NS"
|
||||||
check_resource deploy cert-manager "$CERTMGR_NS"
|
check_resource deploy cert-manager "$CERTMGR_NS"
|
||||||
@ -261,10 +321,12 @@ pod_filter="opentofu|garage|openbao|kong|cert-manager"
|
|||||||
if [[ "$ENABLE_KERBEROS" == "1" ]]; then
|
if [[ "$ENABLE_KERBEROS" == "1" ]]; then
|
||||||
pod_filter="opentofu|garage|openbao|auth|kong|cert-manager"
|
pod_filter="opentofu|garage|openbao|auth|kong|cert-manager"
|
||||||
fi
|
fi
|
||||||
analyze_pods "$ARGOCD_NS" "argocd"
|
|
||||||
analyze_pods "$NS" "$pod_filter"
|
analyze_pods "$NS" "$pod_filter"
|
||||||
analyze_pods "$KONG_NS" "kong"
|
analyze_pods "$KONG_NS" "kong"
|
||||||
analyze_pods "$CERTMGR_NS" "cert-manager"
|
analyze_pods "$CERTMGR_NS" "cert-manager"
|
||||||
|
if [[ "$REGISTRY_CHECK" -eq 1 && "$MODE" != "k3d" ]]; then
|
||||||
|
analyze_pods "$REGISTRY_NS" "registry"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -n "$blocked_lines" ]]; then
|
if [[ -n "$blocked_lines" ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
@ -313,12 +375,10 @@ _workloads_for_comp() {
|
|||||||
local _comp="$1"
|
local _comp="$1"
|
||||||
local _ns="${COMP_NAMESPACE[$_comp]:-$NS}"
|
local _ns="${COMP_NAMESPACE[$_comp]:-$NS}"
|
||||||
case "$_comp" in
|
case "$_comp" in
|
||||||
argocd)
|
registry)
|
||||||
echo "statefulset argocd-application-controller $ARGOCD_NS"
|
if [[ "$REGISTRY_CHECK" -eq 1 && "$MODE" != "k3d" ]]; then
|
||||||
for _d in argocd-server argocd-repo-server argocd-dex-server \
|
echo "deployment registry $REGISTRY_NS"
|
||||||
argocd-applicationset-controller argocd-notifications-controller argocd-redis; do
|
fi
|
||||||
echo "deployment $_d $ARGOCD_NS"
|
|
||||||
done
|
|
||||||
;;
|
;;
|
||||||
openbao)
|
openbao)
|
||||||
if kubectl -n "$_ns" get statefulset openbao >/dev/null 2>&1; then
|
if kubectl -n "$_ns" get statefulset openbao >/dev/null 2>&1; then
|
||||||
@ -347,10 +407,12 @@ _recheck_pods() {
|
|||||||
COMP_NAMESPACE=()
|
COMP_NAMESPACE=()
|
||||||
blocked_lines=""
|
blocked_lines=""
|
||||||
blocked=0
|
blocked=0
|
||||||
analyze_pods "$ARGOCD_NS" "argocd"
|
|
||||||
analyze_pods "$NS" "$pod_filter"
|
analyze_pods "$NS" "$pod_filter"
|
||||||
analyze_pods "$KONG_NS" "kong"
|
analyze_pods "$KONG_NS" "kong"
|
||||||
analyze_pods "$CERTMGR_NS" "cert-manager"
|
analyze_pods "$CERTMGR_NS" "cert-manager"
|
||||||
|
if [[ "$REGISTRY_CHECK" -eq 1 && "$MODE" != "k3d" ]]; then
|
||||||
|
analyze_pods "$REGISTRY_NS" "registry"
|
||||||
|
fi
|
||||||
if [[ -n "$blocked_lines" ]]; then
|
if [[ -n "$blocked_lines" ]]; then
|
||||||
blocked=1
|
blocked=1
|
||||||
fi
|
fi
|
||||||
@ -358,8 +420,9 @@ _recheck_pods() {
|
|||||||
|
|
||||||
# Print the name of each component that currently has blocked pods.
|
# Print the name of each component that currently has blocked pods.
|
||||||
_blocked_comps() {
|
_blocked_comps() {
|
||||||
for _bc in argocd openbao opentofu garage auth kong certmgr; do
|
for _bc in registry openbao opentofu garage auth kong certmgr; do
|
||||||
[[ "$_bc" == "auth" && "$ENABLE_KERBEROS" != "1" ]] && continue
|
[[ "$_bc" == "auth" && "$ENABLE_KERBEROS" != "1" ]] && continue
|
||||||
|
[[ "$_bc" == "registry" && ( "$REGISTRY_CHECK" -ne 1 || "$MODE" == "k3d" ) ]] && continue
|
||||||
[[ ${BLOCKED_COUNT["$_bc"]:-0} -gt 0 ]] && echo "$_bc"
|
[[ ${BLOCKED_COUNT["$_bc"]:-0} -gt 0 ]] && echo "$_bc"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|||||||
@ -92,7 +92,7 @@ def _configure_unbuffered_io():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ProleInstallerBase:
|
class ProleInstaller:
|
||||||
"""Shared business logic for both silent and interactive installer modes.
|
"""Shared business logic for both silent and interactive installer modes.
|
||||||
|
|
||||||
Subclasses must provide a :meth:`_get_input` implementation that bridges
|
Subclasses must provide a :meth:`_get_input` implementation that bridges
|
||||||
@ -1509,6 +1509,169 @@ class ProleInstallerBase:
|
|||||||
phase = ((ns.get("status") or {}).get("phase") or "").strip()
|
phase = ((ns.get("status") or {}).get("phase") or "").strip()
|
||||||
return phase == "Terminating"
|
return phase == "Terminating"
|
||||||
|
|
||||||
|
# ------------------------------------------ blocked cluster reconciliation
|
||||||
|
def _classify_blocked_pod(self, env: dict, pod: dict) -> dict:
|
||||||
|
"""Classify common pod blockers into stable, testable categories.
|
||||||
|
|
||||||
|
Returns a dict with:
|
||||||
|
- `type`: primary classification (storage-first)
|
||||||
|
- `types`: list of all detected classification tags
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ = env # reserved for future use
|
||||||
|
meta = pod.get("metadata") or {}
|
||||||
|
status = pod.get("status") or {}
|
||||||
|
|
||||||
|
name = (meta.get("name") or "").strip()
|
||||||
|
ns = (meta.get("namespace") or "").strip()
|
||||||
|
phase = (status.get("phase") or "").strip()
|
||||||
|
|
||||||
|
types: list[str] = []
|
||||||
|
|
||||||
|
# ContainerCreating / ImagePullBackOff etc.
|
||||||
|
for cs in status.get("containerStatuses") or []:
|
||||||
|
waiting = ((cs.get("state") or {}).get("waiting") or {})
|
||||||
|
reason = (waiting.get("reason") or "").strip()
|
||||||
|
if reason == "ContainerCreating":
|
||||||
|
types.append("container_creating")
|
||||||
|
elif reason:
|
||||||
|
types.append(f"container_waiting_{reason.lower()}")
|
||||||
|
|
||||||
|
# Unschedulable reasons.
|
||||||
|
for cond in status.get("conditions") or []:
|
||||||
|
try:
|
||||||
|
if (cond.get("type") or "").strip() != "PodScheduled":
|
||||||
|
continue
|
||||||
|
if (cond.get("status") or "").strip() != "False":
|
||||||
|
continue
|
||||||
|
if (cond.get("reason") or "").strip() != "Unschedulable":
|
||||||
|
continue
|
||||||
|
|
||||||
|
msg = ((cond.get("message") or "").strip() or "").lower()
|
||||||
|
if "persistent volumes" in msg and "bind" in msg:
|
||||||
|
types.append("unschedulable_due_to_pv_binding")
|
||||||
|
if "didn't match pod's node affinity" in msg or "node affinity/selector" in msg:
|
||||||
|
types.append("unschedulable_due_to_node_affinity")
|
||||||
|
if "had untolerated taint" in msg:
|
||||||
|
types.append("unschedulable_due_to_untolerated_taint")
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Primary type: prefer storage/root-cause categories.
|
||||||
|
primary = "ok"
|
||||||
|
for preferred in (
|
||||||
|
"unschedulable_due_to_pv_binding",
|
||||||
|
"unschedulable_due_to_node_affinity",
|
||||||
|
"unschedulable_due_to_untolerated_taint",
|
||||||
|
"container_creating",
|
||||||
|
):
|
||||||
|
if preferred in types:
|
||||||
|
primary = preferred
|
||||||
|
break
|
||||||
|
if primary == "ok" and phase in ("Pending", "Unknown") and types:
|
||||||
|
primary = types[0]
|
||||||
|
|
||||||
|
# Dedupe while keeping order.
|
||||||
|
seen: set[str] = set()
|
||||||
|
deduped: list[str] = []
|
||||||
|
for t in types:
|
||||||
|
if t not in seen:
|
||||||
|
seen.add(t)
|
||||||
|
deduped.append(t)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pod": f"{ns}/{name}" if ns and name else (name or ns or "<unknown>"),
|
||||||
|
"namespace": ns,
|
||||||
|
"name": name,
|
||||||
|
"phase": phase,
|
||||||
|
"type": primary,
|
||||||
|
"types": deduped,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _reconcile_blocked_cluster_state(self, env: dict, service_ns: str) -> None:
|
||||||
|
"""Detect hard cluster blockers and raise before running deployment scripts.
|
||||||
|
|
||||||
|
This is intentionally conservative: it attempts safe PV claimRef repair
|
||||||
|
and then surfaces clear diagnostics for scheduling/storage blockers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
svc_ns = (service_ns or "").strip() or self._service_namespace()
|
||||||
|
|
||||||
|
# Always attempt safe stale Released PV claimRef repair first.
|
||||||
|
try:
|
||||||
|
self._repair_stale_released_pvs(env, reset=False)
|
||||||
|
except Exception as e:
|
||||||
|
self.err(f"[WARN] Stale PV claimRef repair failed: {e}")
|
||||||
|
|
||||||
|
pods_data = self._kubectl_get_json(env, ["get", "pod", "-A", "-o", "json"]) or {}
|
||||||
|
pods = pods_data.get("items") or []
|
||||||
|
classified = [self._classify_blocked_pod(env, p) for p in pods]
|
||||||
|
blocked = [c for c in classified if (c.get("type") or "") != "ok"]
|
||||||
|
|
||||||
|
# If we can't positively identify blocked pods, do not block deployment.
|
||||||
|
# This keeps behavior conservative when `kubectl` is unavailable.
|
||||||
|
if not blocked:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Downstream missingness: surface but treat separately.
|
||||||
|
missing: list[str] = []
|
||||||
|
for kind in ("svc", "deploy"):
|
||||||
|
try:
|
||||||
|
res = subprocess.run(
|
||||||
|
["kubectl", "-n", svc_ns, "get", kind, "prole-svc-kong"],
|
||||||
|
env=env,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
if res.returncode != 0:
|
||||||
|
missing.append(f"{kind}/prole-svc-kong")
|
||||||
|
except FileNotFoundError:
|
||||||
|
# If kubectl isn't available, don't guess missing resources.
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
all_types: set[str] = set()
|
||||||
|
for b in blocked:
|
||||||
|
for t in b.get("types") or []:
|
||||||
|
all_types.add(t)
|
||||||
|
|
||||||
|
# Group summary for easier scanning.
|
||||||
|
storage_types = [t for t in sorted(all_types) if "pv_binding" in t]
|
||||||
|
sched_types = [
|
||||||
|
t
|
||||||
|
for t in sorted(all_types)
|
||||||
|
if "node_affinity" in t or "untolerated_taint" in t
|
||||||
|
]
|
||||||
|
container_types = [t for t in sorted(all_types) if t.startswith("container_")]
|
||||||
|
|
||||||
|
lines: list[str] = ["Cluster prerequisites are blocked"]
|
||||||
|
if storage_types or any("pv_binding" in (b.get("type") or "") for b in blocked):
|
||||||
|
lines.append("\nStorage blockers:")
|
||||||
|
for t in storage_types or ["unschedulable_due_to_pv_binding"]:
|
||||||
|
lines.append(f"- {t}")
|
||||||
|
if sched_types:
|
||||||
|
lines.append("\nScheduling blockers:")
|
||||||
|
for t in sched_types:
|
||||||
|
lines.append(f"- {t}")
|
||||||
|
if container_types:
|
||||||
|
lines.append("\nContainer blockers:")
|
||||||
|
for t in container_types:
|
||||||
|
lines.append(f"- {t}")
|
||||||
|
if blocked:
|
||||||
|
lines.append("\nBlocked pods:")
|
||||||
|
for b in blocked:
|
||||||
|
lines.append(
|
||||||
|
f"- {b.get('pod')} type={b.get('type')} types={','.join(b.get('types') or [])}"
|
||||||
|
)
|
||||||
|
if missing:
|
||||||
|
lines.append("\nDownstream missing resources:")
|
||||||
|
for m in sorted(set(missing)):
|
||||||
|
lines.append(f"- {m}")
|
||||||
|
|
||||||
|
raise Exception("\n".join(lines))
|
||||||
|
|
||||||
def _ensure_namespace_ready(self, env: dict, name: str, timeout_s: int = 90) -> None:
|
def _ensure_namespace_ready(self, env: dict, name: str, timeout_s: int = 90) -> None:
|
||||||
"""Ensure a namespace is usable for deployments.
|
"""Ensure a namespace is usable for deployments.
|
||||||
|
|
||||||
@ -2251,16 +2414,36 @@ class ProleInstallerBase:
|
|||||||
images = {img for img in images if "supabase" not in img}
|
images = {img for img in images if "supabase" not in img}
|
||||||
|
|
||||||
if include_kerberos_proxy:
|
if include_kerberos_proxy:
|
||||||
images.add("ghcr.io/bsharp-tech/prole-kerberos-proxy:latest")
|
krb_img = os.environ.get("KRB5_AD_PROXY_IMAGE", "alpine/socat")
|
||||||
|
if krb_img:
|
||||||
|
images.add(krb_img)
|
||||||
|
|
||||||
return sorted(images)
|
return sorted(images)
|
||||||
|
|
||||||
|
def _get_docker_import_dir(self) -> str:
|
||||||
|
"""Best-effort accessor for a docker-import directory.
|
||||||
|
|
||||||
|
GUI uses a Tk variable with `.get()`, while console flows tend to store a
|
||||||
|
plain string. This helper normalizes both.
|
||||||
|
"""
|
||||||
|
|
||||||
|
val = getattr(self, "docker_import_dir", None)
|
||||||
|
if val is None:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
getter = getattr(val, "get", None)
|
||||||
|
if callable(getter):
|
||||||
|
val = getter()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return (str(val) if val is not None else "").strip()
|
||||||
|
|
||||||
def _prepull_images_to_registry(
|
def _prepull_images_to_registry(
|
||||||
self,
|
self,
|
||||||
include_supabase: bool,
|
include_supabase: bool,
|
||||||
include_kerberos_proxy: bool,
|
include_kerberos_proxy: bool,
|
||||||
log: Callable[[str], None] | None = None,
|
log: Callable[[str], None] | None = None,
|
||||||
) -> None:
|
) -> bool:
|
||||||
def _log(msg: str):
|
def _log(msg: str):
|
||||||
if log:
|
if log:
|
||||||
try:
|
try:
|
||||||
@ -2269,48 +2452,120 @@ class ProleInstallerBase:
|
|||||||
pass
|
pass
|
||||||
self.log(msg)
|
self.log(msg)
|
||||||
|
|
||||||
|
cluster_env = ""
|
||||||
|
try:
|
||||||
|
cluster_env = self._get_input("init_cluster.cluster_env", "")
|
||||||
|
except Exception:
|
||||||
|
cluster_env = ""
|
||||||
|
|
||||||
|
if cluster_env and not _local_registry_enabled(cluster_env):
|
||||||
|
_log("[SKIP] Local registry disabled; skipping image pre-pull.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
mode = _deployment_mode_from_env(cluster_env)
|
||||||
|
if mode == "k3s":
|
||||||
|
# Keep behavior consistent across installers: k3s relies on in-cluster
|
||||||
|
# image pulls/import paths rather than a local docker registry.
|
||||||
|
_log("[SKIP] Image pre-pull is not supported in k3s mode.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
ensure_fn = getattr(self, "ensure_local_registry_available", None)
|
||||||
|
if not callable(ensure_fn):
|
||||||
|
ensure_fn = getattr(self, "_ensure_local_registry_available", None)
|
||||||
|
info = ensure_fn() if callable(ensure_fn) else None
|
||||||
|
if not info:
|
||||||
|
_log("[SKIP] Local registry unavailable; skipping image pre-pull.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
registry, _cluster_registry = info
|
||||||
|
|
||||||
images = self._collect_dependent_images(
|
images = self._collect_dependent_images(
|
||||||
include_supabase, include_kerberos_proxy
|
include_supabase, include_kerberos_proxy
|
||||||
)
|
)
|
||||||
if not images:
|
if not images:
|
||||||
_log("[SKIP] No dependent images to prepull.")
|
_log("[SKIP] No dependent images found to pre-pull.")
|
||||||
return
|
return True
|
||||||
reg = getattr(self, "local_registry_url", None)
|
|
||||||
if not reg:
|
overall_ok = True
|
||||||
reg = self.prole_cfg_data.get("Docker Build", {}).get("LOCAL_REGISTRY", "")
|
import_dir = self._get_docker_import_dir()
|
||||||
if not reg:
|
|
||||||
_log("[SKIP] No local registry configured; skipping prepull.")
|
|
||||||
return
|
|
||||||
host, _, port_str = reg.partition(":")
|
|
||||||
try:
|
|
||||||
port = int(port_str)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
port = 5000
|
|
||||||
if not _http_ping_registry(host, port):
|
|
||||||
_log(f"[SKIP] Registry {reg} not reachable; skipping prepull.")
|
|
||||||
return
|
|
||||||
for image in images:
|
for image in images:
|
||||||
_log(f"[INFO] Pulling {image} ...")
|
local_tag = image
|
||||||
try:
|
if not image.startswith(f"{registry}/"):
|
||||||
rc = subprocess.run(
|
local_tag = f"{registry}/{image}"
|
||||||
["docker", "pull", image],
|
|
||||||
|
# 1. Check if already in local registry
|
||||||
|
_log(f"[INFO] Checking if {image} exists in local registry...")
|
||||||
|
check_reg = subprocess.run(
|
||||||
|
["docker", "pull", local_tag], capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if check_reg.returncode == 0:
|
||||||
|
_log(f"[OK] {image} already exists in local registry as {local_tag}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 2. Check if we already have it in local docker daemon
|
||||||
|
check_local = subprocess.run(
|
||||||
|
["docker", "image", "inspect", image],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=300,
|
|
||||||
)
|
)
|
||||||
if rc.returncode != 0:
|
found = check_local.returncode == 0
|
||||||
self.err(
|
|
||||||
f"[WARN] docker pull failed for {image}: {(rc.stderr or '').strip()}"
|
if not found and import_dir and os.path.isdir(import_dir):
|
||||||
|
# 3. Check import directory
|
||||||
|
safe_name = image.replace("/", "_").replace(":", "_")
|
||||||
|
tar_path = Path(import_dir) / f"{safe_name}.tar"
|
||||||
|
if tar_path.exists():
|
||||||
|
_log(f"[INFO] Found {tar_path} in import directory, loading...")
|
||||||
|
load = subprocess.run(
|
||||||
|
["docker", "load", "-i", str(tar_path)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
)
|
)
|
||||||
continue
|
if load.returncode == 0:
|
||||||
except Exception as e:
|
found = True
|
||||||
self.err(f"[WARN] docker pull exception for {image}: {e}")
|
|
||||||
continue
|
|
||||||
ok = _push_docker_image(image, reg)
|
|
||||||
if ok:
|
|
||||||
_log(f"[OK] Pushed {image} → {reg}")
|
|
||||||
else:
|
else:
|
||||||
self.err(f"[WARN] Push failed for {image} → {reg}")
|
overall_ok = False
|
||||||
|
self.err(f"[WARN] Failed to load {tar_path}: {load.stderr}")
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
# 4. Pull from upstream
|
||||||
|
_log(f"[INFO] Pulling {image} from upstream...")
|
||||||
|
pull = subprocess.run(
|
||||||
|
["docker", "pull", image], capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if pull.returncode != 0:
|
||||||
|
overall_ok = False
|
||||||
|
self.err(pull.stdout or "")
|
||||||
|
self.err(pull.stderr or "")
|
||||||
|
self.err(f"[ERROR] docker pull failed for {image}")
|
||||||
|
continue
|
||||||
|
found = True
|
||||||
|
|
||||||
|
# If we have the image locally, tag and push to local registry
|
||||||
|
if found:
|
||||||
|
if local_tag != image:
|
||||||
|
tag = subprocess.run(
|
||||||
|
["docker", "tag", image, local_tag],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if tag.returncode != 0:
|
||||||
|
overall_ok = False
|
||||||
|
self.err(tag.stdout or "")
|
||||||
|
self.err(tag.stderr or "")
|
||||||
|
self.err(
|
||||||
|
f"[ERROR] docker tag failed for {image} to {local_tag}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
_log(f"[INFO] Pushing {local_tag} to local registry...")
|
||||||
|
if not _push_docker_image(local_tag, log_fn=self.log):
|
||||||
|
overall_ok = False
|
||||||
|
continue
|
||||||
|
_log(f"[OK] Stored {image} in local registry as {local_tag}")
|
||||||
|
|
||||||
|
return overall_ok
|
||||||
|
|
||||||
|
|
||||||
class _TeeWriter:
|
class _TeeWriter:
|
||||||
@ -2351,7 +2606,7 @@ class _TeeWriter:
|
|||||||
return getattr(self._original, name)
|
return getattr(self._original, name)
|
||||||
|
|
||||||
|
|
||||||
class ProleSilentInstaller(ProleInstallerBase):
|
class ProleConsoleInstaller(ProleInstaller):
|
||||||
"""Console-based unattended installer driven by prole.cfg inputs."""
|
"""Console-based unattended installer driven by prole.cfg inputs."""
|
||||||
|
|
||||||
silent = True
|
silent = True
|
||||||
@ -3128,237 +3383,6 @@ class ProleSilentInstaller(ProleInstallerBase):
|
|||||||
|
|
||||||
self.prole_cfg_data["Docker Build"] = db
|
self.prole_cfg_data["Docker Build"] = db
|
||||||
|
|
||||||
def _collect_dependent_images(
|
|
||||||
self, include_supabase: bool, include_kerberos_proxy: bool
|
|
||||||
) -> list[str]:
|
|
||||||
images = set()
|
|
||||||
for rel_dir in ("k8s/prole", "k8s/openbao"):
|
|
||||||
base_dir = self.project_root / rel_dir
|
|
||||||
if not base_dir.exists():
|
|
||||||
continue
|
|
||||||
images.update(_collect_images_from_files(list(base_dir.glob("*.yaml"))))
|
|
||||||
|
|
||||||
if include_supabase:
|
|
||||||
supa_home = _resolve_supabase_home(self.project_root)
|
|
||||||
if supa_home:
|
|
||||||
docker_dir = supa_home / "docker"
|
|
||||||
compose_files = [docker_dir / "docker-compose.yml"]
|
|
||||||
if os.environ.get("SUPABASE_USE_DEV_COMPOSE") == "1":
|
|
||||||
compose_files.append(docker_dir / "dev" / "docker-compose.dev.yml")
|
|
||||||
images.update(_collect_images_from_files(compose_files))
|
|
||||||
|
|
||||||
if not include_supabase:
|
|
||||||
images = {img for img in images if "supabase" not in img}
|
|
||||||
|
|
||||||
if include_kerberos_proxy:
|
|
||||||
krb_img = os.environ.get("KRB5_AD_PROXY_IMAGE", "alpine/socat")
|
|
||||||
if krb_img:
|
|
||||||
images.add(krb_img)
|
|
||||||
|
|
||||||
return sorted(images)
|
|
||||||
|
|
||||||
def _prepull_images_to_registry(
|
|
||||||
self,
|
|
||||||
include_supabase: bool,
|
|
||||||
include_kerberos_proxy: bool,
|
|
||||||
log: Callable[[str], None] | None = None,
|
|
||||||
) -> bool:
|
|
||||||
def _log(msg: str):
|
|
||||||
if log:
|
|
||||||
try:
|
|
||||||
log(msg)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self.log(msg)
|
|
||||||
|
|
||||||
if not _local_registry_enabled(self._get_input("init_cluster.cluster_env", "")):
|
|
||||||
_log("[SKIP] Local registry disabled; skipping image pre-pull.")
|
|
||||||
return False
|
|
||||||
mode = _deployment_mode_from_env(
|
|
||||||
self._get_input("init_cluster.cluster_env", "")
|
|
||||||
)
|
|
||||||
|
|
||||||
if mode == "k3s":
|
|
||||||
return self._prepull_images_k3s(include_supabase, include_kerberos_proxy)
|
|
||||||
|
|
||||||
if mode != "k3d":
|
|
||||||
self.log("[SKIP] Local registry pre-pull only supported for k3d/k3s.")
|
|
||||||
return False
|
|
||||||
info = self._ensure_local_registry_available()
|
|
||||||
if not info:
|
|
||||||
self.err("[WARN] Local registry unavailable; skipping image pre-pull.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
registry, _cluster_registry = info
|
|
||||||
images = self._collect_dependent_images(
|
|
||||||
include_supabase, include_kerberos_proxy
|
|
||||||
)
|
|
||||||
if not images:
|
|
||||||
self.log("[INFO] No dependent images found to pre-pull.")
|
|
||||||
return True
|
|
||||||
|
|
||||||
overall_ok = True
|
|
||||||
import_dir = self.docker_import_dir
|
|
||||||
|
|
||||||
for image in images:
|
|
||||||
local_tag = image
|
|
||||||
if not image.startswith(f"{registry}/"):
|
|
||||||
local_tag = f"{registry}/{image}"
|
|
||||||
|
|
||||||
# 1. Check if already in local registry
|
|
||||||
self.log(f"[INFO] Checking if {image} exists in local registry...")
|
|
||||||
check_reg = subprocess.run(
|
|
||||||
["docker", "pull", local_tag], capture_output=True, text=True
|
|
||||||
)
|
|
||||||
if check_reg.returncode == 0:
|
|
||||||
self.log(
|
|
||||||
f"[OK] {image} already exists in local registry as {local_tag}"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 2. Check if we already have it in local docker daemon
|
|
||||||
check_local = subprocess.run(
|
|
||||||
["docker", "image", "inspect", image], capture_output=True, text=True
|
|
||||||
)
|
|
||||||
found = check_local.returncode == 0
|
|
||||||
|
|
||||||
if not found and import_dir and os.path.isdir(import_dir):
|
|
||||||
# 3. Check import directory
|
|
||||||
safe_name = image.replace("/", "_").replace(":", "_")
|
|
||||||
tar_path = Path(import_dir) / f"{safe_name}.tar"
|
|
||||||
if tar_path.exists():
|
|
||||||
self.log(f"[INFO] Found {tar_path} in import directory, loading...")
|
|
||||||
load = subprocess.run(
|
|
||||||
["docker", "load", "-i", str(tar_path)],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
if load.returncode == 0:
|
|
||||||
found = True
|
|
||||||
else:
|
|
||||||
self.err(f"[WARN] Failed to load {tar_path}: {load.stderr}")
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
# 4. Pull from Docker Hub
|
|
||||||
self.log(f"[INFO] Pulling {image} from Docker Hub...")
|
|
||||||
pull = subprocess.run(
|
|
||||||
["docker", "pull", image], capture_output=True, text=True
|
|
||||||
)
|
|
||||||
if pull.returncode != 0:
|
|
||||||
overall_ok = False
|
|
||||||
self.err(pull.stdout or "")
|
|
||||||
self.err(pull.stderr or "")
|
|
||||||
self.err(f"[ERROR] docker pull failed for {image}")
|
|
||||||
continue
|
|
||||||
found = True
|
|
||||||
|
|
||||||
# If we have the image locally, tag and push to local registry
|
|
||||||
if found:
|
|
||||||
if local_tag != image:
|
|
||||||
tag = subprocess.run(
|
|
||||||
["docker", "tag", image, local_tag],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
if tag.returncode != 0:
|
|
||||||
overall_ok = False
|
|
||||||
self.err(tag.stdout or "")
|
|
||||||
self.err(tag.stderr or "")
|
|
||||||
self.err(
|
|
||||||
f"[ERROR] docker tag failed for {image} to {local_tag}"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.log(f"[INFO] Pushing {local_tag} to local registry...")
|
|
||||||
if not _push_docker_image(local_tag, log_fn=self.log):
|
|
||||||
overall_ok = False
|
|
||||||
continue
|
|
||||||
self.log(f"[OK] Stored {image} in local registry as {local_tag}")
|
|
||||||
|
|
||||||
return overall_ok
|
|
||||||
|
|
||||||
def _prepull_images_k3s(
|
|
||||||
self, include_supabase: bool, include_kerberos_proxy: bool
|
|
||||||
) -> bool:
|
|
||||||
"""Stage images for k3s by pushing to the in-cluster `registry:2`."""
|
|
||||||
db_cfg = self.prole_cfg_data.get("Docker Build", {}) or {}
|
|
||||||
registry = (db_cfg.get("LOCAL_REGISTRY") or "").strip()
|
|
||||||
if not registry:
|
|
||||||
self.log(
|
|
||||||
"[SKIP] No LOCAL_REGISTRY configured for k3s; skipping image staging."
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
images = self._collect_dependent_images(
|
|
||||||
include_supabase, include_kerberos_proxy
|
|
||||||
)
|
|
||||||
if not images:
|
|
||||||
self.log("[INFO] No dependent images found to stage for k3s.")
|
|
||||||
return True
|
|
||||||
|
|
||||||
overall_ok = True
|
|
||||||
import_dir = self.docker_import_dir
|
|
||||||
|
|
||||||
for image in images:
|
|
||||||
remote_tag = (
|
|
||||||
f"{registry}/{image}" if not image.startswith(f"{registry}/") else image
|
|
||||||
)
|
|
||||||
|
|
||||||
# 1. Check local docker daemon
|
|
||||||
check_local = subprocess.run(
|
|
||||||
["docker", "image", "inspect", image], capture_output=True, text=True
|
|
||||||
)
|
|
||||||
found = check_local.returncode == 0
|
|
||||||
|
|
||||||
# 2. Try docker-import directory
|
|
||||||
if not found and import_dir and os.path.isdir(import_dir):
|
|
||||||
safe_name = image.replace("/", "_").replace(":", "_")
|
|
||||||
tar_path = Path(import_dir) / f"{safe_name}.tar"
|
|
||||||
if tar_path.exists():
|
|
||||||
self.log(f"[INFO] Loading {tar_path} from import directory...")
|
|
||||||
load = subprocess.run(
|
|
||||||
["docker", "load", "-i", str(tar_path)],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
if load.returncode == 0:
|
|
||||||
found = True
|
|
||||||
else:
|
|
||||||
self.err(f"[WARN] Failed to load {tar_path}: {load.stderr}")
|
|
||||||
|
|
||||||
# 3. Pull from upstream
|
|
||||||
if not found:
|
|
||||||
self.log(f"[INFO] Pulling {image} ...")
|
|
||||||
pull = subprocess.run(
|
|
||||||
["docker", "pull", image], capture_output=True, text=True
|
|
||||||
)
|
|
||||||
if pull.returncode != 0:
|
|
||||||
self.err(f"[WARN] docker pull failed for {image}")
|
|
||||||
else:
|
|
||||||
found = True
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 4. Tag and push to k3s registry (skip if already present)
|
|
||||||
if remote_tag != image:
|
|
||||||
subprocess.run(
|
|
||||||
["docker", "tag", image, remote_tag], capture_output=True, text=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if _registry_image_ref_exists(remote_tag):
|
|
||||||
self.log(f"[SKIP] {remote_tag} already exists in registry; skipping push.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.log(f"[INFO] Pushing {remote_tag} to {registry} ...")
|
|
||||||
if _push_docker_image(remote_tag, log_fn=self.log):
|
|
||||||
self.log(f"[OK] Pushed {image} to {registry}")
|
|
||||||
else:
|
|
||||||
overall_ok = False
|
|
||||||
self.err(f"[ERROR] Failed to push {remote_tag} to {registry}")
|
|
||||||
|
|
||||||
return overall_ok
|
|
||||||
|
|
||||||
def _fetch_k3s_kubeconfig(self) -> Path | None:
|
def _fetch_k3s_kubeconfig(self) -> Path | None:
|
||||||
"""Fetch a fresh kubeconfig from k3s via the Ansible playbook."""
|
"""Fetch a fresh kubeconfig from k3s via the Ansible playbook."""
|
||||||
kubeconfig = self.project_root / "prole-k3s.kubeconfig"
|
kubeconfig = self.project_root / "prole-k3s.kubeconfig"
|
||||||
@ -4408,25 +4432,17 @@ class ProleSilentInstaller(ProleInstallerBase):
|
|||||||
|
|
||||||
if env_key == "service":
|
if env_key == "service":
|
||||||
try:
|
try:
|
||||||
argocd_ns = self._argocd_namespace()
|
|
||||||
registry_ns = self._registry_namespace()
|
registry_ns = self._registry_namespace()
|
||||||
env = self._script_env_for_namespace(self._service_namespace())
|
env = self._script_env_for_namespace(self._service_namespace())
|
||||||
env["ARGOCD_NAMESPACE"] = argocd_ns
|
|
||||||
env["REGISTRY_NAMESPACE"] = registry_ns
|
env["REGISTRY_NAMESPACE"] = registry_ns
|
||||||
self.log("==> Registry/ArgoCD preflight")
|
self.log("==> Registry preflight")
|
||||||
self._run_script(
|
self._run_script(
|
||||||
"init_registry.sh",
|
"init_registry.sh",
|
||||||
args=[
|
args=["-n", registry_ns, "update"],
|
||||||
"-n",
|
|
||||||
argocd_ns,
|
|
||||||
"--registry-namespace",
|
|
||||||
registry_ns,
|
|
||||||
"update",
|
|
||||||
],
|
|
||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.err(f"[WARN] Registry/ArgoCD preflight failed: {e}")
|
self.err(f"[WARN] Registry preflight failed: {e}")
|
||||||
|
|
||||||
# If this run was launched with a reset request, attempt to reclaim any
|
# If this run was launched with a reset request, attempt to reclaim any
|
||||||
# strongly-matched stale Released PVs early, before later deploy/init
|
# strongly-matched stale Released PVs early, before later deploy/init
|
||||||
@ -4510,6 +4526,11 @@ class ProleSilentInstaller(ProleInstallerBase):
|
|||||||
# Avoid deploying into a namespace that is currently being deleted.
|
# Avoid deploying into a namespace that is currently being deleted.
|
||||||
self._ensure_namespace_ready(env, service_ns)
|
self._ensure_namespace_ready(env, service_ns)
|
||||||
|
|
||||||
|
# Refuse to proceed when the cluster has hard prerequisites blocked
|
||||||
|
# (e.g. PV binding / scheduling issues). This prevents blind retries
|
||||||
|
# and surfaces actionable diagnostics.
|
||||||
|
self._reconcile_blocked_cluster_state(env=env, service_ns=service_ns)
|
||||||
|
|
||||||
# Check status before update
|
# Check status before update
|
||||||
status_args = ["-n", service_ns]
|
status_args = ["-n", service_ns]
|
||||||
if kerberos_enabled:
|
if kerberos_enabled:
|
||||||
@ -5297,7 +5318,7 @@ def _prepare_k3s_pipeline(
|
|||||||
|
|
||||||
# Load config to resolve namespace and secrets for pipeline tfvars
|
# Load config to resolve namespace and secrets for pipeline tfvars
|
||||||
_log("==> Loading installer config...\n")
|
_log("==> Loading installer config...\n")
|
||||||
installer = ProleSilentInstaller(controller, str(cfg_path))
|
installer = ProleConsoleInstaller(controller, str(cfg_path))
|
||||||
try:
|
try:
|
||||||
existing_inputs = installer._load_inputs_from_cfg()
|
existing_inputs = installer._load_inputs_from_cfg()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@ -1311,7 +1311,12 @@ def _default_opentofu_pipeline_url() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _push_docker_image(image_tag: str, log_fn=None) -> bool:
|
def _push_docker_image(image_tag: str, log_fn=None) -> bool:
|
||||||
"""Push Docker image with skopeo fallback for insecure registries."""
|
"""Push Docker image with skopeo fallback.
|
||||||
|
|
||||||
|
If Docker push fails due to TLS/CA issues, this will try `skopeo copy`.
|
||||||
|
When a registry CA is configured (via `PROLE_REGISTRY_CA_CERT` or
|
||||||
|
`PROLE_REGISTRY_CERT_DIR`), it will prefer a secure skopeo push.
|
||||||
|
"""
|
||||||
|
|
||||||
def _log(msg):
|
def _log(msg):
|
||||||
if log_fn:
|
if log_fn:
|
||||||
@ -1333,11 +1338,93 @@ def _push_docker_image(image_tag: str, log_fn=None) -> bool:
|
|||||||
f"[WARN] Docker push failed: {res.stderr.strip() if res.stderr else 'unknown error'}\n"
|
f"[WARN] Docker push failed: {res.stderr.strip() if res.stderr else 'unknown error'}\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. Try skopeo fallback for insecure registry
|
# 2. Try skopeo fallback
|
||||||
skopeo = shutil.which("skopeo")
|
skopeo = shutil.which("skopeo")
|
||||||
if skopeo:
|
if skopeo:
|
||||||
|
cert_dir = (os.environ.get("PROLE_REGISTRY_CERT_DIR") or "").strip()
|
||||||
|
cert_file = (
|
||||||
|
os.environ.get("PROLE_REGISTRY_CA_CERT")
|
||||||
|
or os.environ.get("REGISTRY_CA_CERT")
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
# Convenience auto-detection (matches how Ansible ships certs from this repo).
|
||||||
|
# If a registry host is `myrddin.prole.org:5000`, look for
|
||||||
|
# `ssl/prole/myrddin-registry.crt` when no env override is provided.
|
||||||
|
if not cert_dir and not cert_file:
|
||||||
|
try:
|
||||||
|
ref = (image_tag or "").strip()
|
||||||
|
if "/" in ref:
|
||||||
|
registry = ref.split("/", 1)[0]
|
||||||
|
host_only = registry.split(":", 1)[0]
|
||||||
|
short = host_only.split(".", 1)[0]
|
||||||
|
candidates = [
|
||||||
|
PROJECT_ROOT / "ssl" / "prole" / f"{short}-registry.crt",
|
||||||
|
PROJECT_ROOT / "ssl" / "prole" / f"{host_only}-registry.crt",
|
||||||
|
]
|
||||||
|
for c in candidates:
|
||||||
|
if c.is_file():
|
||||||
|
cert_file = str(c)
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Prefer a secure skopeo push if we have a CA configured.
|
||||||
|
try:
|
||||||
|
if cert_dir:
|
||||||
|
p = Path(cert_dir).expanduser()
|
||||||
|
if p.is_dir():
|
||||||
|
_log("Retrying with skopeo (TLS verify, custom cert dir) ...\n")
|
||||||
|
cmd = [
|
||||||
|
skopeo,
|
||||||
|
"copy",
|
||||||
|
"--dest-tls-verify=true",
|
||||||
|
"--dest-cert-dir",
|
||||||
|
str(p),
|
||||||
|
f"docker-daemon:{image_tag}",
|
||||||
|
f"docker://{image_tag}",
|
||||||
|
]
|
||||||
|
res2 = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if res2.returncode == 0:
|
||||||
|
_log(f"[OK] Pushed {image_tag} using skopeo (TLS verified)\n")
|
||||||
|
return True
|
||||||
|
_log(
|
||||||
|
f"[WARN] Skopeo TLS-verified push failed: {res2.stderr.strip() if res2.stderr else 'unknown error'}\n"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_log(f"[WARN] Unable to use PROLE_REGISTRY_CERT_DIR: {e}\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if cert_file:
|
||||||
|
p = Path(cert_file).expanduser()
|
||||||
|
if p.is_file():
|
||||||
|
_log("Retrying with skopeo (TLS verify, custom CA cert) ...\n")
|
||||||
|
with tempfile.TemporaryDirectory(prefix="prole-registry-cert-") as td:
|
||||||
|
ca_dest = Path(td) / "ca.crt"
|
||||||
|
shutil.copyfile(str(p), str(ca_dest))
|
||||||
|
cmd = [
|
||||||
|
skopeo,
|
||||||
|
"copy",
|
||||||
|
"--dest-tls-verify=true",
|
||||||
|
"--dest-cert-dir",
|
||||||
|
td,
|
||||||
|
f"docker-daemon:{image_tag}",
|
||||||
|
f"docker://{image_tag}",
|
||||||
|
]
|
||||||
|
res2 = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if res2.returncode == 0:
|
||||||
|
_log(
|
||||||
|
f"[OK] Pushed {image_tag} using skopeo (TLS verified)\n"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
_log(
|
||||||
|
f"[WARN] Skopeo TLS-verified push failed: {res2.stderr.strip() if res2.stderr else 'unknown error'}\n"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_log(f"[WARN] Unable to use PROLE_REGISTRY_CA_CERT: {e}\n")
|
||||||
|
|
||||||
|
# Backward-compatible fallback (explicitly insecure).
|
||||||
_log("Retrying with skopeo (insecure registry) ...\n")
|
_log("Retrying with skopeo (insecure registry) ...\n")
|
||||||
# skopeo copy --dest-tls-verify=false docker-daemon:TAG docker://TAG
|
|
||||||
cmd = [
|
cmd = [
|
||||||
skopeo,
|
skopeo,
|
||||||
"copy",
|
"copy",
|
||||||
@ -1345,12 +1432,12 @@ def _push_docker_image(image_tag: str, log_fn=None) -> bool:
|
|||||||
f"docker-daemon:{image_tag}",
|
f"docker-daemon:{image_tag}",
|
||||||
f"docker://{image_tag}",
|
f"docker://{image_tag}",
|
||||||
]
|
]
|
||||||
res2 = subprocess.run(cmd, capture_output=True, text=True)
|
res3 = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
if res2.returncode == 0:
|
if res3.returncode == 0:
|
||||||
_log(f"[OK] Pushed {image_tag} using skopeo\n")
|
_log(f"[OK] Pushed {image_tag} using skopeo\n")
|
||||||
return True
|
return True
|
||||||
_log(
|
_log(
|
||||||
f"[ERROR] Skopeo push failed: {res2.stderr.strip() if res2.stderr else 'unknown error'}\n"
|
f"[ERROR] Skopeo push failed: {res3.stderr.strip() if res3.stderr else 'unknown error'}\n"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
_log("[ERROR] skopeo not found; cannot retry push.\n")
|
_log("[ERROR] skopeo not found; cannot retry push.\n")
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from installer import prole_conf
|
from installer import prole_conf
|
||||||
from installer.core.controller import ProleController
|
from installer.core.controller import ProleController
|
||||||
from installer.core.actions import ProleSilentInstaller
|
from installer.core.actions import ProleConsoleInstaller
|
||||||
|
|
||||||
|
|
||||||
def _project_root() -> Path:
|
def _project_root() -> Path:
|
||||||
@ -81,7 +81,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
cfg = str(prole_conf.entrypoint_path(prole_conf.resolve_prole_conf_dir(root)))
|
cfg = str(prole_conf.entrypoint_path(prole_conf.resolve_prole_conf_dir(root)))
|
||||||
|
|
||||||
controller = ProleController(project_root=root, verbose=bool(args.verbose), cfg_path=Path(cfg))
|
controller = ProleController(project_root=root, verbose=bool(args.verbose), cfg_path=Path(cfg))
|
||||||
installer = ProleSilentInstaller(controller, cfg_path=cfg)
|
installer = ProleConsoleInstaller(controller, cfg_path=cfg)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
existing_inputs = installer._load_inputs_from_cfg()
|
existing_inputs = installer._load_inputs_from_cfg()
|
||||||
|
|||||||
@ -72,8 +72,8 @@ if platform.system() == "Darwin":
|
|||||||
|
|
||||||
from installer.core.env import * # noqa: F401,F403
|
from installer.core.env import * # noqa: F401,F403
|
||||||
from installer.core.actions import (
|
from installer.core.actions import (
|
||||||
ProleInstallerBase,
|
ProleInstaller,
|
||||||
ProleSilentInstaller,
|
ProleConsoleInstaller,
|
||||||
_prepare_k3s_pipeline,
|
_prepare_k3s_pipeline,
|
||||||
_reset_k3s_namespace,
|
_reset_k3s_namespace,
|
||||||
)
|
)
|
||||||
@ -98,6 +98,7 @@ from installer.ui.screens.security import SecurityScreenMixin
|
|||||||
from installer.ui.screens.ollama import OllamaScreenMixin
|
from installer.ui.screens.ollama import OllamaScreenMixin
|
||||||
from installer.ui.screens.supabase import SupabaseScreenMixin
|
from installer.ui.screens.supabase import SupabaseScreenMixin
|
||||||
from installer.ui.screens.gitops import GitOpsScreenMixin
|
from installer.ui.screens.gitops import GitOpsScreenMixin
|
||||||
|
from installer.ui.screens.argocd import ArgoCDScreenMixin
|
||||||
from installer.ui.screens.docker import DockerScreenMixin
|
from installer.ui.screens.docker import DockerScreenMixin
|
||||||
from installer.ui.screens.build import BuildScreenMixin
|
from installer.ui.screens.build import BuildScreenMixin
|
||||||
from installer.ui.screens.packaging import PackagingScreenMixin
|
from installer.ui.screens.packaging import PackagingScreenMixin
|
||||||
@ -110,7 +111,7 @@ from installer.ui.screens.cfg import ConfigMixin
|
|||||||
# Composed installer class
|
# Composed installer class
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
class ProleInstaller(
|
class ProleInstaller(
|
||||||
ProleInstallerBase,
|
ProleInstaller,
|
||||||
ScreenBaseMixin,
|
ScreenBaseMixin,
|
||||||
NavigationMixin,
|
NavigationMixin,
|
||||||
WelcomeScreenMixin,
|
WelcomeScreenMixin,
|
||||||
@ -125,6 +126,7 @@ class ProleInstaller(
|
|||||||
OllamaScreenMixin,
|
OllamaScreenMixin,
|
||||||
SupabaseScreenMixin,
|
SupabaseScreenMixin,
|
||||||
GitOpsScreenMixin,
|
GitOpsScreenMixin,
|
||||||
|
ArgoCDScreenMixin,
|
||||||
DockerScreenMixin,
|
DockerScreenMixin,
|
||||||
BuildScreenMixin,
|
BuildScreenMixin,
|
||||||
PackagingScreenMixin,
|
PackagingScreenMixin,
|
||||||
@ -152,6 +154,7 @@ class ProleInstaller(
|
|||||||
"init_cluster.kerberos_enabled": "kerberos_enabled",
|
"init_cluster.kerberos_enabled": "kerberos_enabled",
|
||||||
"init_cluster.supabase_enabled": "supabase_enabled",
|
"init_cluster.supabase_enabled": "supabase_enabled",
|
||||||
"init_cluster.gitops_enabled": "gitops_enabled",
|
"init_cluster.gitops_enabled": "gitops_enabled",
|
||||||
|
"init_cluster.argocd_enabled": "argocd_enabled",
|
||||||
"init_cluster.at_rest_encryption_enabled": "at_rest_encryption_enabled",
|
"init_cluster.at_rest_encryption_enabled": "at_rest_encryption_enabled",
|
||||||
"init_cluster.selected_kubectx": "selected_kubectx",
|
"init_cluster.selected_kubectx": "selected_kubectx",
|
||||||
"init_cluster.k3s_server_url": "k3s_server_url",
|
"init_cluster.k3s_server_url": "k3s_server_url",
|
||||||
@ -160,6 +163,7 @@ class ProleInstaller(
|
|||||||
"ollama_config.server_port": "ollama_server_port",
|
"ollama_config.server_port": "ollama_server_port",
|
||||||
"ollama_config.model": "ollama_model",
|
"ollama_config.model": "ollama_model",
|
||||||
"gitops.namespace": "gitops_namespace",
|
"gitops.namespace": "gitops_namespace",
|
||||||
|
"argocd.namespace": "argocd_namespace",
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_input(self, key: str, default: str | None = None) -> str:
|
def _get_input(self, key: str, default: str | None = None) -> str:
|
||||||
@ -353,6 +357,7 @@ class ProleInstaller(
|
|||||||
("Initialization Scripts", "init_scripts"),
|
("Initialization Scripts", "init_scripts"),
|
||||||
("Kerberos Authentication", "kerberos_config"),
|
("Kerberos Authentication", "kerberos_config"),
|
||||||
("GitOps", "gitops_config"),
|
("GitOps", "gitops_config"),
|
||||||
|
("ArgoCD", "argocd_config"),
|
||||||
("Supabase", "supabase_config"),
|
("Supabase", "supabase_config"),
|
||||||
("Ollama", "ollama_config"),
|
("Ollama", "ollama_config"),
|
||||||
("Deployment", "init_cnpg_deploy"),
|
("Deployment", "init_cnpg_deploy"),
|
||||||
@ -430,6 +435,10 @@ class ProleInstaller(
|
|||||||
# Must exist before reading prole.cfg so saved values are applied (avoid silent fallbacks).
|
# Must exist before reading prole.cfg so saved values are applied (avoid silent fallbacks).
|
||||||
self.cluster_env = tk.StringVar(value="dev")
|
self.cluster_env = tk.StringVar(value="dev")
|
||||||
self.service_namespace = tk.StringVar(value="default")
|
self.service_namespace = tk.StringVar(value="default")
|
||||||
|
self.gitops_enabled = tk.BooleanVar(value=False)
|
||||||
|
self.gitops_namespace = tk.StringVar(value="gitea")
|
||||||
|
self.argocd_enabled = tk.BooleanVar(value=False)
|
||||||
|
self.argocd_namespace = tk.StringVar(value="argocd")
|
||||||
|
|
||||||
# Load existing docker_import_dir from prole.cfg
|
# Load existing docker_import_dir from prole.cfg
|
||||||
try:
|
try:
|
||||||
@ -460,6 +469,9 @@ class ProleInstaller(
|
|||||||
saved_service_ns = _expand_cfg_value(
|
saved_service_ns = _expand_cfg_value(
|
||||||
cfg["Global"].get("SERVICE_NAMESPACE", ""), cfg_vars
|
cfg["Global"].get("SERVICE_NAMESPACE", ""), cfg_vars
|
||||||
).strip()
|
).strip()
|
||||||
|
saved_argocd_ns = _expand_cfg_value(
|
||||||
|
cfg["Global"].get("ARGOCD_NAMESPACE", ""), cfg_vars
|
||||||
|
).strip()
|
||||||
saved_gitops_ns = (
|
saved_gitops_ns = (
|
||||||
_expand_cfg_value(
|
_expand_cfg_value(
|
||||||
cfg.get("GitOps", "GITOPS_NAMESPACE", fallback=""),
|
cfg.get("GitOps", "GITOPS_NAMESPACE", fallback=""),
|
||||||
@ -468,6 +480,18 @@ class ProleInstaller(
|
|||||||
if cfg.has_section("GitOps")
|
if cfg.has_section("GitOps")
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
|
saved_argocd_enabled = (
|
||||||
|
_expand_cfg_value(
|
||||||
|
cfg.get(
|
||||||
|
"Optional Features",
|
||||||
|
"ARGOCD_ENABLED",
|
||||||
|
fallback="",
|
||||||
|
),
|
||||||
|
cfg_vars,
|
||||||
|
).strip()
|
||||||
|
if cfg.has_section("Optional Features")
|
||||||
|
else ""
|
||||||
|
)
|
||||||
saved_gitops_enabled = (
|
saved_gitops_enabled = (
|
||||||
_expand_cfg_value(
|
_expand_cfg_value(
|
||||||
cfg.get(
|
cfg.get(
|
||||||
@ -510,6 +534,14 @@ class ProleInstaller(
|
|||||||
self.prole_cfg_data["Global"][
|
self.prole_cfg_data["Global"][
|
||||||
"SERVICE_NAMESPACE"
|
"SERVICE_NAMESPACE"
|
||||||
] = saved_service_ns
|
] = saved_service_ns
|
||||||
|
if saved_argocd_ns:
|
||||||
|
try:
|
||||||
|
self.argocd_namespace.set(saved_argocd_ns)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.prole_cfg_data.setdefault("Global", {})[
|
||||||
|
"ARGOCD_NAMESPACE"
|
||||||
|
] = saved_argocd_ns
|
||||||
if saved_gitops_ns:
|
if saved_gitops_ns:
|
||||||
try:
|
try:
|
||||||
self.gitops_namespace.set(saved_gitops_ns)
|
self.gitops_namespace.set(saved_gitops_ns)
|
||||||
@ -518,6 +550,15 @@ class ProleInstaller(
|
|||||||
self.prole_cfg_data.setdefault("GitOps", {})[
|
self.prole_cfg_data.setdefault("GitOps", {})[
|
||||||
"NAMESPACE"
|
"NAMESPACE"
|
||||||
] = saved_gitops_ns
|
] = saved_gitops_ns
|
||||||
|
if saved_argocd_enabled:
|
||||||
|
val = saved_argocd_enabled.lower() in ("true", "1", "yes")
|
||||||
|
try:
|
||||||
|
self.argocd_enabled.set(val)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.prole_cfg_data.setdefault("Optional Features", {})[
|
||||||
|
"ARGOCD_ENABLED"
|
||||||
|
] = str(val)
|
||||||
if saved_gitops_enabled:
|
if saved_gitops_enabled:
|
||||||
val = saved_gitops_enabled.lower() in ("true", "1", "yes")
|
val = saved_gitops_enabled.lower() in ("true", "1", "yes")
|
||||||
try:
|
try:
|
||||||
@ -623,9 +664,7 @@ class ProleInstaller(
|
|||||||
self.kerberos_password = tk.StringVar()
|
self.kerberos_password = tk.StringVar()
|
||||||
self.kerberos_kdc = tk.StringVar()
|
self.kerberos_kdc = tk.StringVar()
|
||||||
self.supabase_enabled = tk.BooleanVar(value=False)
|
self.supabase_enabled = tk.BooleanVar(value=False)
|
||||||
self.gitops_enabled = tk.BooleanVar(value=False)
|
|
||||||
self.at_rest_encryption_enabled = tk.BooleanVar(value=True)
|
self.at_rest_encryption_enabled = tk.BooleanVar(value=True)
|
||||||
self.gitops_namespace = tk.StringVar(value="gitea")
|
|
||||||
self.ollama_server_host = tk.StringVar()
|
self.ollama_server_host = tk.StringVar()
|
||||||
self.ollama_server_port = tk.StringVar(value=DEFAULT_OLLAMA_PORT)
|
self.ollama_server_port = tk.StringVar(value=DEFAULT_OLLAMA_PORT)
|
||||||
self.ollama_model = tk.StringVar()
|
self.ollama_model = tk.StringVar()
|
||||||
@ -676,6 +715,7 @@ class ProleInstaller(
|
|||||||
self._register_canvas_renderer(
|
self._register_canvas_renderer(
|
||||||
"supabase_config", self._render_supabase_config_page
|
"supabase_config", self._render_supabase_config_page
|
||||||
)
|
)
|
||||||
|
self._register_canvas_renderer("argocd_config", self._render_argocd_config_page)
|
||||||
self._register_canvas_renderer("gitops_config", self._render_gitops_config_page)
|
self._register_canvas_renderer("gitops_config", self._render_gitops_config_page)
|
||||||
self._register_canvas_renderer("init_cluster", self._render_init_cluster_page)
|
self._register_canvas_renderer("init_cluster", self._render_init_cluster_page)
|
||||||
self._register_canvas_renderer("init_db_build", self._render_init_db_build_page)
|
self._register_canvas_renderer("init_db_build", self._render_init_db_build_page)
|
||||||
@ -707,6 +747,7 @@ class ProleInstaller(
|
|||||||
self._register_page("init_db_build", None)
|
self._register_page("init_db_build", None)
|
||||||
self._register_page("init_scripts", None)
|
self._register_page("init_scripts", None)
|
||||||
self._register_page("kerberos_config", None)
|
self._register_page("kerberos_config", None)
|
||||||
|
self._register_page("argocd_config", None)
|
||||||
self._register_page("gitops_config", None)
|
self._register_page("gitops_config", None)
|
||||||
self._register_page("supabase_config", None)
|
self._register_page("supabase_config", None)
|
||||||
self._register_page("ollama_config", None)
|
self._register_page("ollama_config", None)
|
||||||
@ -825,7 +866,7 @@ def main():
|
|||||||
|
|
||||||
if args.silent:
|
if args.silent:
|
||||||
_ensure_ansible_vault_credentials(prompt_ui=False)
|
_ensure_ansible_vault_credentials(prompt_ui=False)
|
||||||
installer = ProleSilentInstaller(
|
installer = ProleConsoleInstaller(
|
||||||
controller, args.config, reset_cluster=args.reset
|
controller, args.config, reset_cluster=args.reset
|
||||||
)
|
)
|
||||||
if log_file_path:
|
if log_file_path:
|
||||||
|
|||||||
276
installer/ui/screens/argocd.py
Normal file
276
installer/ui/screens/argocd.py
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
"""Optional ArgoCD deployment screen.
|
||||||
|
|
||||||
|
This screen intentionally mirrors the layout and behavior of the GitOps (Gitea)
|
||||||
|
optional feature screen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import tkinter as tk
|
||||||
|
|
||||||
|
from installer import screen as ui
|
||||||
|
from installer.core.env import PROJECT_ROOT, _normalize_cluster_env
|
||||||
|
|
||||||
|
|
||||||
|
class ArgoCDScreenMixin:
|
||||||
|
def _render_argocd_config_page(self):
|
||||||
|
"""Optional feature screen: ArgoCD."""
|
||||||
|
self.clear_overlay_widgets()
|
||||||
|
self._clear_canvas_items()
|
||||||
|
|
||||||
|
ui.create_section_header(
|
||||||
|
self, "Optional Feature: ArgoCD", "Deploy ArgoCD into your cluster"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Enable toggle
|
||||||
|
enabled_check = tk.Checkbutton(
|
||||||
|
self.bg_canvas,
|
||||||
|
text="Enable ArgoCD",
|
||||||
|
variable=self.argocd_enabled,
|
||||||
|
bg="white",
|
||||||
|
fg="black",
|
||||||
|
activebackground="white",
|
||||||
|
activeforeground="black",
|
||||||
|
selectcolor="white",
|
||||||
|
command=self._on_argocd_toggle,
|
||||||
|
font=("SF Pro Text", 12, "bold"),
|
||||||
|
)
|
||||||
|
enabled_window = self.bg_canvas.create_window(
|
||||||
|
48, 180, window=enabled_check, anchor="nw"
|
||||||
|
)
|
||||||
|
self._canvas_items.append(enabled_window)
|
||||||
|
self._overlay_widgets.append(enabled_check)
|
||||||
|
|
||||||
|
# Namespace entry
|
||||||
|
ui.canvas_text(
|
||||||
|
self,
|
||||||
|
48,
|
||||||
|
290,
|
||||||
|
"Kubernetes Namespace",
|
||||||
|
fill="black",
|
||||||
|
font=("SF Pro Text", 12, "bold"),
|
||||||
|
)
|
||||||
|
ns_entry = tk.Entry(
|
||||||
|
self.bg_canvas,
|
||||||
|
textvariable=self.argocd_namespace,
|
||||||
|
bg="white",
|
||||||
|
fg="black",
|
||||||
|
insertbackground="black",
|
||||||
|
highlightbackground="#CCCCCC",
|
||||||
|
highlightthickness=1,
|
||||||
|
relief="flat",
|
||||||
|
font=("SF Pro Text", 11),
|
||||||
|
)
|
||||||
|
ns_window = self.bg_canvas.create_window(
|
||||||
|
48, 315, window=ns_entry, anchor="nw", width=220, height=30
|
||||||
|
)
|
||||||
|
self._canvas_items.append(ns_window)
|
||||||
|
self._overlay_widgets.append(ns_entry)
|
||||||
|
|
||||||
|
y = 360
|
||||||
|
initial_status = "Ready" if self.argocd_enabled.get() else "Disabled"
|
||||||
|
self._argocd_status_var = tk.StringVar(value=initial_status)
|
||||||
|
status_item = ui.canvas_text(
|
||||||
|
self,
|
||||||
|
48,
|
||||||
|
y,
|
||||||
|
f"Status: {initial_status}",
|
||||||
|
fill="black",
|
||||||
|
font=("SF Pro Text", 12),
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_status_text(*_):
|
||||||
|
try:
|
||||||
|
self.bg_canvas.itemconfig(
|
||||||
|
status_item, text=f"Status: {self._argocd_status_var.get()}"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._argocd_status_var.trace_add("write", update_status_text)
|
||||||
|
|
||||||
|
# Console output
|
||||||
|
self._argocd_console = self._create_console_output(
|
||||||
|
y=400, title="Deployment Output", width=880, height=330
|
||||||
|
)
|
||||||
|
|
||||||
|
# Deploy button
|
||||||
|
self._argocd_deploy_button = tk.Button(
|
||||||
|
self.bg_canvas,
|
||||||
|
text="Deploy ArgoCD",
|
||||||
|
command=self.run_argocd_deploy,
|
||||||
|
bg="#F5F5DC",
|
||||||
|
fg="black",
|
||||||
|
activebackground="#E5E5D5",
|
||||||
|
activeforeground="black",
|
||||||
|
highlightbackground="#F5F5DC",
|
||||||
|
highlightcolor="#F5F5DC",
|
||||||
|
highlightthickness=0,
|
||||||
|
relief="flat",
|
||||||
|
bd=0,
|
||||||
|
cursor="hand2",
|
||||||
|
disabledforeground="#8B8B7A",
|
||||||
|
font=("SF Pro Text", 11),
|
||||||
|
padx=20,
|
||||||
|
pady=10,
|
||||||
|
)
|
||||||
|
btn_window = self.bg_canvas.create_window(
|
||||||
|
48, 760, window=self._argocd_deploy_button, anchor="nw"
|
||||||
|
)
|
||||||
|
self._overlay_widgets.append(self._argocd_deploy_button)
|
||||||
|
self._canvas_items.append(btn_window)
|
||||||
|
|
||||||
|
def run_argocd_deploy(self):
|
||||||
|
if getattr(self, "_argocd_deploying", False):
|
||||||
|
return
|
||||||
|
if not self.argocd_enabled.get():
|
||||||
|
self._argocd_status_var.set("Disabled")
|
||||||
|
self.update_footer()
|
||||||
|
return
|
||||||
|
|
||||||
|
self._argocd_deploying = True
|
||||||
|
self._argocd_deploy_button.configure(state="disabled")
|
||||||
|
self._argocd_status_var.set("Deploying...")
|
||||||
|
self._argocd_success = False
|
||||||
|
self._argocd_console.clear()
|
||||||
|
self.update_footer()
|
||||||
|
|
||||||
|
def worker():
|
||||||
|
namespace = (
|
||||||
|
(self.argocd_namespace.get() or "").strip()
|
||||||
|
or os.environ.get("ARGOCD_NAMESPACE")
|
||||||
|
or "argocd"
|
||||||
|
)
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["PROLE_HOME"] = str(PROJECT_ROOT)
|
||||||
|
env["PROLE_SERVICE"] = str(PROJECT_ROOT)
|
||||||
|
env["NAMESPACE"] = namespace
|
||||||
|
env["ARGOCD_NAMESPACE"] = namespace
|
||||||
|
|
||||||
|
self._argocd_console.write("Starting ArgoCD deployment...\n")
|
||||||
|
|
||||||
|
script_path = PROJECT_ROOT / "etc" / "init_argocd.sh"
|
||||||
|
if not script_path.exists():
|
||||||
|
self._argocd_console.write(f"Error: {script_path} not found.\n")
|
||||||
|
self.safe_after(
|
||||||
|
lambda: self._argocd_status_var.set("Failed (Script not found)")
|
||||||
|
)
|
||||||
|
self.safe_after(
|
||||||
|
lambda: self._argocd_deploy_button.configure(state="normal")
|
||||||
|
)
|
||||||
|
self._argocd_deploying = False
|
||||||
|
return
|
||||||
|
|
||||||
|
mode = (os.environ.get("PROLE_MODE") or "").strip()
|
||||||
|
if not mode:
|
||||||
|
cluster_env = _normalize_cluster_env(self.cluster_env.get())
|
||||||
|
if cluster_env == "dev":
|
||||||
|
mode = "k3d"
|
||||||
|
elif cluster_env in ("service", "prod"):
|
||||||
|
mode = "k3s" if cluster_env == "service" else "k8s"
|
||||||
|
else:
|
||||||
|
mode = "k3d"
|
||||||
|
args = ["--mode", mode, "--namespace", namespace, "update"]
|
||||||
|
|
||||||
|
cfg_path = None
|
||||||
|
try:
|
||||||
|
if self._cfg_path_override and self._cfg_path_override.exists():
|
||||||
|
cfg_path = self._cfg_path_override
|
||||||
|
else:
|
||||||
|
conf_dir = self._resolve_prole_conf_dir()
|
||||||
|
candidate = conf_dir / "prole.cfg"
|
||||||
|
if candidate.exists():
|
||||||
|
cfg_path = candidate
|
||||||
|
if not cfg_path:
|
||||||
|
from installer import prole_conf
|
||||||
|
|
||||||
|
candidate = prole_conf.entrypoint_path(
|
||||||
|
prole_conf.resolve_prole_conf_dir(PROJECT_ROOT)
|
||||||
|
)
|
||||||
|
if candidate.exists():
|
||||||
|
cfg_path = candidate
|
||||||
|
except Exception:
|
||||||
|
cfg_path = None
|
||||||
|
if cfg_path:
|
||||||
|
args.extend(["-c", str(cfg_path)])
|
||||||
|
|
||||||
|
self._argocd_console.write(f"Mode: {mode}\n")
|
||||||
|
cmd = ["bash", str(script_path)] + args
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
cwd=str(PROJECT_ROOT),
|
||||||
|
env=env,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
bufsize=1,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self._argocd_console.write(f"Failed to start deploy: {e}\n")
|
||||||
|
self.safe_after(
|
||||||
|
lambda: self._argocd_status_var.set("Failed (Launch error)")
|
||||||
|
)
|
||||||
|
self.safe_after(
|
||||||
|
lambda: self._argocd_deploy_button.configure(state="normal")
|
||||||
|
)
|
||||||
|
self._argocd_deploying = False
|
||||||
|
return
|
||||||
|
|
||||||
|
if proc.stdout:
|
||||||
|
for line in iter(proc.stdout.readline, ""):
|
||||||
|
if line:
|
||||||
|
self._argocd_console.write(line)
|
||||||
|
proc.stdout.close()
|
||||||
|
rc = proc.wait()
|
||||||
|
|
||||||
|
if rc == 0:
|
||||||
|
self._argocd_success = True
|
||||||
|
self.safe_after(
|
||||||
|
lambda: self._argocd_status_var.set("Deployed Successfully")
|
||||||
|
)
|
||||||
|
self._argocd_console.write(
|
||||||
|
"\nArgoCD deployment completed successfully.\n"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._argocd_success = False
|
||||||
|
self.safe_after(
|
||||||
|
lambda: self._argocd_status_var.set(f"Failed (Code {rc})")
|
||||||
|
)
|
||||||
|
self._argocd_console.write(
|
||||||
|
f"\nArgoCD deployment failed with exit code {rc}.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._argocd_deploying = False
|
||||||
|
self.safe_after(
|
||||||
|
lambda: self._argocd_deploy_button.configure(state="normal")
|
||||||
|
)
|
||||||
|
self.safe_after(self.update_footer)
|
||||||
|
|
||||||
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
|
def _on_argocd_toggle(self):
|
||||||
|
enabled = self.argocd_enabled.get()
|
||||||
|
try:
|
||||||
|
self.prole_cfg_data["Optional Features"]["ARGOCD_ENABLED"] = str(enabled)
|
||||||
|
# Keep compatibility with other parts of the installer that expect Global.ARGOCD_NAMESPACE.
|
||||||
|
self.prole_cfg_data.setdefault("Global", {})["ARGOCD_NAMESPACE"] = (
|
||||||
|
(self.argocd_namespace.get() or "").strip() or "argocd"
|
||||||
|
)
|
||||||
|
self._save_prole_cfg()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not enabled:
|
||||||
|
self._argocd_success = False
|
||||||
|
if hasattr(self, "_argocd_status_var"):
|
||||||
|
self._argocd_status_var.set("Disabled")
|
||||||
|
try:
|
||||||
|
self._argocd_console.write("ArgoCD disabled. Deployment skipped.\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.update_footer()
|
||||||
@ -159,6 +159,7 @@ class ScreenBaseMixin:
|
|||||||
"supabase_config",
|
"supabase_config",
|
||||||
"ollama_config",
|
"ollama_config",
|
||||||
"gitops_config",
|
"gitops_config",
|
||||||
|
"argocd_config",
|
||||||
"database_options",
|
"database_options",
|
||||||
"create_installer",
|
"create_installer",
|
||||||
)
|
)
|
||||||
|
|||||||
@ -34,6 +34,10 @@ from installer.config import _collect_cfg_vars, _encrypt_cfg_secret, _expand_cfg
|
|||||||
class ConfigMixin:
|
class ConfigMixin:
|
||||||
"""prole.cfg persistence, input snapshot collection and port-forward management."""
|
"""prole.cfg persistence, input snapshot collection and port-forward management."""
|
||||||
|
|
||||||
|
# Namespace typing can trigger many trace events; debounce propagation to avoid
|
||||||
|
# incremental substring rewrites across unrelated fields.
|
||||||
|
NAMESPACE_PROPAGATE_DEBOUNCE_MS = 250
|
||||||
|
|
||||||
def _save_prole_cfg(self):
|
def _save_prole_cfg(self):
|
||||||
"""Generates prole.cfg; master configuration file containing all values used in install process."""
|
"""Generates prole.cfg; master configuration file containing all values used in install process."""
|
||||||
try:
|
try:
|
||||||
@ -330,6 +334,9 @@ class ConfigMixin:
|
|||||||
"init_cluster.supabase_enabled", _get_var(self.supabase_enabled, False)
|
"init_cluster.supabase_enabled", _get_var(self.supabase_enabled, False)
|
||||||
)
|
)
|
||||||
_set_bool("init_cluster.gitops_enabled", _get_var(self.gitops_enabled, False))
|
_set_bool("init_cluster.gitops_enabled", _get_var(self.gitops_enabled, False))
|
||||||
|
_set_bool(
|
||||||
|
"init_cluster.argocd_enabled", _get_var(self.argocd_enabled, False)
|
||||||
|
)
|
||||||
_set_bool(
|
_set_bool(
|
||||||
"init_cluster.kerberos_enabled", _get_var(self.kerberos_enabled, False)
|
"init_cluster.kerberos_enabled", _get_var(self.kerberos_enabled, False)
|
||||||
)
|
)
|
||||||
@ -399,6 +406,9 @@ class ConfigMixin:
|
|||||||
# GitOps
|
# GitOps
|
||||||
_set("gitops.namespace", _get_var(self.gitops_namespace, "gitea"))
|
_set("gitops.namespace", _get_var(self.gitops_namespace, "gitea"))
|
||||||
|
|
||||||
|
# ArgoCD
|
||||||
|
_set("argocd.namespace", _get_var(self.argocd_namespace, "argocd"))
|
||||||
|
|
||||||
# Disk selection (installer packaging)
|
# Disk selection (installer packaging)
|
||||||
_set("disk_selection.disk_type", _get_var(self.selected_disk_type, "local"))
|
_set("disk_selection.disk_type", _get_var(self.selected_disk_type, "local"))
|
||||||
_set(
|
_set(
|
||||||
@ -915,21 +925,91 @@ class ConfigMixin:
|
|||||||
self.k3s_token.set(token_val)
|
self.k3s_token.set(token_val)
|
||||||
|
|
||||||
def _propagate_namespace_change(self, *args):
|
def _propagate_namespace_change(self, *args):
|
||||||
"""Reacts to namespace change by updating all dependent inputs and config files."""
|
"""Reacts to namespace change by updating dependent inputs and config files.
|
||||||
|
|
||||||
|
This is triggered by a `tk.StringVar` trace while the user types. We debounce
|
||||||
|
the propagation so we only apply a single stable transition from the last
|
||||||
|
synced namespace to the final value (instead of rewriting on each keystroke).
|
||||||
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
new_ns = (self.db_namespace.get() or "").strip()
|
new_ns = (self.db_namespace.get() or "").strip()
|
||||||
except Exception:
|
except Exception:
|
||||||
return
|
return
|
||||||
if not new_ns or new_ns == getattr(self, "_last_synced_ns", None):
|
|
||||||
|
if not new_ns:
|
||||||
return
|
return
|
||||||
|
|
||||||
old_ns = getattr(self, "_last_synced_ns", "")
|
# If there is no Tk root (e.g. some test harnesses), fall back to immediate.
|
||||||
|
root = getattr(self, "root", None)
|
||||||
|
if root is None:
|
||||||
|
self._apply_pending_namespace_change(new_ns)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Debounce: cancel any in-flight scheduled propagation and reschedule.
|
||||||
|
after_id = getattr(self, "_namespace_propagate_after_id", None)
|
||||||
|
if after_id:
|
||||||
|
try:
|
||||||
|
root.after_cancel(after_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._pending_namespace_value = new_ns
|
||||||
|
delay_ms = getattr(self, "_namespace_debounce_ms", None)
|
||||||
|
if delay_ms is None:
|
||||||
|
delay_ms = getattr(self, "NAMESPACE_PROPAGATE_DEBOUNCE_MS", 250)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._namespace_propagate_after_id = root.after(
|
||||||
|
int(delay_ms), self._apply_pending_namespace_change
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
# If scheduling fails, apply immediately.
|
||||||
|
self._apply_pending_namespace_change(new_ns)
|
||||||
|
|
||||||
|
def _apply_pending_namespace_change(self, namespace: str | None = None):
|
||||||
|
"""Apply the pending namespace propagation using a stable old namespace."""
|
||||||
|
|
||||||
|
# Clear the timer id if we were invoked from `after`.
|
||||||
|
try:
|
||||||
|
self._namespace_propagate_after_id = None
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if namespace is None:
|
||||||
|
try:
|
||||||
|
namespace = (getattr(self, "_pending_namespace_value", "") or "").strip()
|
||||||
|
except Exception:
|
||||||
|
namespace = ""
|
||||||
|
|
||||||
|
new_ns = (namespace or "").strip()
|
||||||
|
if not new_ns:
|
||||||
|
return
|
||||||
|
|
||||||
|
old_ns = (getattr(self, "_last_synced_ns", "") or "").strip()
|
||||||
|
if not old_ns:
|
||||||
|
# Initialize sync baseline and persist.
|
||||||
|
try:
|
||||||
self._last_synced_ns = new_ns
|
self._last_synced_ns = new_ns
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if new_ns == old_ns:
|
||||||
|
return
|
||||||
|
|
||||||
# Propagate to inputs and StringVars (e.g. OpenBao paths)
|
# Propagate to inputs and StringVars (e.g. OpenBao paths)
|
||||||
if old_ns:
|
if old_ns:
|
||||||
|
# Do not rewrite independent namespaces.
|
||||||
|
exclude_var_names = {
|
||||||
|
"db_namespace",
|
||||||
|
"service_namespace",
|
||||||
|
"gitops_namespace",
|
||||||
|
"argocd_namespace",
|
||||||
|
}
|
||||||
|
|
||||||
# 1. Sweep StringVars
|
# 1. Sweep StringVars
|
||||||
for attr_name in dir(self):
|
for attr_name in dir(self):
|
||||||
|
if attr_name in exclude_var_names:
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
attr = getattr(self, attr_name)
|
attr = getattr(self, attr_name)
|
||||||
if isinstance(attr, tk.StringVar) and attr != self.db_namespace:
|
if isinstance(attr, tk.StringVar) and attr != self.db_namespace:
|
||||||
@ -942,10 +1022,18 @@ class ConfigMixin:
|
|||||||
# 2. Sweep inputs
|
# 2. Sweep inputs
|
||||||
if hasattr(self, "inputs"):
|
if hasattr(self, "inputs"):
|
||||||
for k, v in list(self.inputs.items()):
|
for k, v in list(self.inputs.items()):
|
||||||
if isinstance(v, str) and old_ns in v:
|
if not isinstance(v, str) or old_ns not in v:
|
||||||
|
continue
|
||||||
|
lk = (k or "").lower()
|
||||||
|
if "service_namespace" in lk or lk.endswith("service_namespace"):
|
||||||
|
continue
|
||||||
self.inputs[k] = v.replace(old_ns, new_ns)
|
self.inputs[k] = v.replace(old_ns, new_ns)
|
||||||
|
|
||||||
# Update environment and save prole.cfg
|
# Update environment and save prole.cfg
|
||||||
|
try:
|
||||||
|
self._last_synced_ns = new_ns
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
try:
|
try:
|
||||||
self._update_env_namespace(new_ns)
|
self._update_env_namespace(new_ns)
|
||||||
# Re-save prole.cfg so it's always in sync
|
# Re-save prole.cfg so it's always in sync
|
||||||
|
|||||||
@ -388,7 +388,7 @@ class ClusterScreenMixin:
|
|||||||
# Traffic light status indicators for each service component
|
# Traffic light status indicators for each service component
|
||||||
self._service_traffic_lights = {}
|
self._service_traffic_lights = {}
|
||||||
components = [
|
components = [
|
||||||
"Argo",
|
"Registry",
|
||||||
"CertMgr",
|
"CertMgr",
|
||||||
"Garage",
|
"Garage",
|
||||||
"Kong",
|
"Kong",
|
||||||
@ -538,7 +538,13 @@ class ClusterScreenMixin:
|
|||||||
return key
|
return key
|
||||||
|
|
||||||
def _get_service_namespace(self) -> str:
|
def _get_service_namespace(self) -> str:
|
||||||
"""Resolve service namespace with UI/env/config fallbacks."""
|
"""Resolve service namespace with UI/env/config fallbacks.
|
||||||
|
|
||||||
|
Safety rule: avoid defaulting to the Kubernetes `default` namespace unless we truly
|
||||||
|
cannot infer a better option.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 1) Explicit user/config/env choice wins.
|
||||||
try:
|
try:
|
||||||
ns = (self.service_namespace.get() or "").strip()
|
ns = (self.service_namespace.get() or "").strip()
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -554,17 +560,126 @@ class ClusterScreenMixin:
|
|||||||
ns = ""
|
ns = ""
|
||||||
if not ns:
|
if not ns:
|
||||||
ns = (os.environ.get("SERVICE_NAMESPACE") or "").strip()
|
ns = (os.environ.get("SERVICE_NAMESPACE") or "").strip()
|
||||||
|
if ns:
|
||||||
|
return ns
|
||||||
|
|
||||||
default_ns = "default"
|
# 2) If the cluster is reachable, infer the common services namespace.
|
||||||
|
inferred = ""
|
||||||
|
try:
|
||||||
|
inferred = (self._infer_common_services_namespace() or "").strip()
|
||||||
|
except Exception:
|
||||||
|
inferred = ""
|
||||||
|
if inferred:
|
||||||
|
return inferred
|
||||||
|
|
||||||
|
# 3) Stable defaults for known environments.
|
||||||
try:
|
try:
|
||||||
# Common services should never fall back to the Kubernetes "default" namespace.
|
|
||||||
# For local dev (k3d) and service (k3s), keep knoe-system stable across restarts.
|
# For local dev (k3d) and service (k3s), keep knoe-system stable across restarts.
|
||||||
if self._cluster_env_key() in ("dev", "service"):
|
if self._cluster_env_key() in ("dev", "service"):
|
||||||
default_ns = "knoe-system"
|
return "knoe-system"
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return ns or default_ns
|
# 4) Absolute last resort.
|
||||||
|
return "default"
|
||||||
|
|
||||||
|
def _infer_common_services_namespace(self) -> str:
|
||||||
|
"""Infer the namespace hosting common services.
|
||||||
|
|
||||||
|
If the cluster is reachable, try to locate an existing Docker Registry workload
|
||||||
|
(image containing `registry:2`). If found, treat that namespace as the common
|
||||||
|
services namespace.
|
||||||
|
|
||||||
|
This method is read-only.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Cache to avoid repeated `kubectl -A` probes during periodic refresh.
|
||||||
|
cache_ttl_s = 60
|
||||||
|
now = time.time()
|
||||||
|
cached_ns = getattr(self, "_common_services_ns_cache", "")
|
||||||
|
cached_at = getattr(self, "_common_services_ns_cache_at", 0.0)
|
||||||
|
if cached_ns and (now - float(cached_at or 0.0)) < cache_ttl_s:
|
||||||
|
return cached_ns
|
||||||
|
|
||||||
|
# Ensure kubeconfig is available when possible (read-only action).
|
||||||
|
try:
|
||||||
|
self._ensure_k3s_kubeconfig_merged()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Build a kubectl command suitable for the active mode.
|
||||||
|
try:
|
||||||
|
mode = self._deployment_mode()
|
||||||
|
except Exception:
|
||||||
|
mode = None
|
||||||
|
try:
|
||||||
|
base_cmd = self._kubectl_base_cmd(mode)
|
||||||
|
except Exception:
|
||||||
|
base_cmd = ["kubectl"]
|
||||||
|
|
||||||
|
# Respect an existing kubeconfig if present.
|
||||||
|
env = os.environ.copy()
|
||||||
|
try:
|
||||||
|
kc = _find_kubeconfig_file(env)
|
||||||
|
except Exception:
|
||||||
|
kc = _find_kubeconfig_file()
|
||||||
|
if kc:
|
||||||
|
env["KUBECONFIG"] = kc
|
||||||
|
|
||||||
|
def _probe(kind: str) -> list[str]:
|
||||||
|
cmd = base_cmd + ["get", kind, "-A", "-o", "json"]
|
||||||
|
res = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=env,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
if res.returncode != 0:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
data = json.loads(res.stdout or "{}")
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
out = []
|
||||||
|
for item in (data.get("items") or []):
|
||||||
|
try:
|
||||||
|
ns = (item.get("metadata") or {}).get("namespace") or ""
|
||||||
|
containers = (
|
||||||
|
(((item.get("spec") or {}).get("template") or {}).get("spec") or {}).get(
|
||||||
|
"containers"
|
||||||
|
)
|
||||||
|
or []
|
||||||
|
)
|
||||||
|
for c in containers:
|
||||||
|
img = (c.get("image") or "").strip()
|
||||||
|
if "registry:2" in img:
|
||||||
|
if ns:
|
||||||
|
out.append(ns)
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return out
|
||||||
|
|
||||||
|
namespaces = []
|
||||||
|
# Deployments are the most common shape; fall back to StatefulSets.
|
||||||
|
namespaces.extend(_probe("deploy"))
|
||||||
|
if not namespaces:
|
||||||
|
namespaces.extend(_probe("sts"))
|
||||||
|
|
||||||
|
# Prefer knoe-system if present, otherwise prefer a non-default namespace.
|
||||||
|
ns_choice = ""
|
||||||
|
if namespaces:
|
||||||
|
if "knoe-system" in namespaces:
|
||||||
|
ns_choice = "knoe-system"
|
||||||
|
else:
|
||||||
|
non_default = [n for n in namespaces if n and n != "default"]
|
||||||
|
ns_choice = (non_default[0] if non_default else namespaces[0]) or ""
|
||||||
|
|
||||||
|
if ns_choice:
|
||||||
|
setattr(self, "_common_services_ns_cache", ns_choice)
|
||||||
|
setattr(self, "_common_services_ns_cache_at", now)
|
||||||
|
return ns_choice
|
||||||
|
|
||||||
def _on_cluster_env_change(self, *args):
|
def _on_cluster_env_change(self, *args):
|
||||||
# Keep configuration environment selection explicit and stable.
|
# Keep configuration environment selection explicit and stable.
|
||||||
@ -1478,6 +1593,13 @@ class ClusterScreenMixin:
|
|||||||
threading.Thread(target=_cleanup_worker, daemon=True).start()
|
threading.Thread(target=_cleanup_worker, daemon=True).start()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# On save, immediately run a services status refresh so the UI reflects the
|
||||||
|
# newly persisted namespace/cluster settings.
|
||||||
|
try:
|
||||||
|
self._verify_k3s_services()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _cluster_status_snapshot(self) -> dict:
|
def _cluster_status_snapshot(self) -> dict:
|
||||||
@ -1544,54 +1666,27 @@ class ClusterScreenMixin:
|
|||||||
kong_status = None
|
kong_status = None
|
||||||
if status.get("cluster_ok"):
|
if status.get("cluster_ok"):
|
||||||
kong_status = self._dashboard_kong_status(status.get("env"))
|
kong_status = self._dashboard_kong_status(status.get("env"))
|
||||||
self._set_cluster_env_message("Assess Actions", "#34c759")
|
self._set_cluster_env_message("Observe", "#34c759")
|
||||||
|
|
||||||
# Repair step: reset Dashboard (Kong) pods if unhealthy, with cooldown
|
# Observe-only: do not mutate the cluster automatically.
|
||||||
if kong_status and kong_status.get("reset_pods"):
|
anomalies = []
|
||||||
now = time.time()
|
try:
|
||||||
if now - self._kong_last_repair_at >= self._kong_repair_cooldown_s:
|
if kong_status and not kong_status.get("ok", True):
|
||||||
|
anomalies.append("dashboard")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if self._authority_context_missing():
|
||||||
|
anomalies.append("authority")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if anomalies:
|
||||||
self._set_cluster_env_message(
|
self._set_cluster_env_message(
|
||||||
"Execute Actions (REPAIR): reset Dashboard (Kong) pod(s)",
|
"Notice: issues detected. Click Repair to reconcile.",
|
||||||
"#34c759",
|
"#ff9f0a",
|
||||||
)
|
|
||||||
pods = kong_status.get("reset_pods") or []
|
|
||||||
cmd = (
|
|
||||||
self._kubectl_base_cmd(status.get("env"))
|
|
||||||
+ ["delete", "pod", "-n", "kubernetes-dashboard"]
|
|
||||||
+ pods
|
|
||||||
)
|
|
||||||
rc, _out = self._run_cmd_capture(cmd, timeout=10)
|
|
||||||
if rc != 0:
|
|
||||||
self._set_cluster_env_message(
|
|
||||||
"Notice: Dashboard (Kong) repair failed",
|
|
||||||
"#ff3b30",
|
|
||||||
clear_after_ms=6000,
|
clear_after_ms=6000,
|
||||||
)
|
)
|
||||||
notice_active = True
|
notice_active = True
|
||||||
else:
|
|
||||||
self._kong_last_repair_at = now
|
|
||||||
# Give the cluster a moment to recreate pods before re-check
|
|
||||||
time.sleep(2)
|
|
||||||
self._set_cluster_env_message("Collect Status", "#34c759")
|
|
||||||
status = self._cluster_status_snapshot()
|
|
||||||
else:
|
|
||||||
self._set_cluster_env_message(
|
|
||||||
"Execute Actions (REPAIR): skipped (recently attempted)",
|
|
||||||
"#34c759",
|
|
||||||
clear_after_ms=1500,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._set_cluster_env_message(
|
|
||||||
"Execute Actions (REPAIR): none needed",
|
|
||||||
"#34c759",
|
|
||||||
clear_after_ms=1500,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Run the repair pipeline if anomalies are detected on a ready cluster
|
|
||||||
try:
|
|
||||||
self._maybe_run_repair_pipeline(status, kong_status)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def update_ui():
|
def update_ui():
|
||||||
if hasattr(self, "k3d_status_label"):
|
if hasattr(self, "k3d_status_label"):
|
||||||
@ -1870,18 +1965,12 @@ class ClusterScreenMixin:
|
|||||||
def _parse_service_status_lines(self, lines: list[str]) -> dict[str, bool]:
|
def _parse_service_status_lines(self, lines: list[str]) -> dict[str, bool]:
|
||||||
"""Parse [OK]/[FAIL] lines from status_common_services.sh output.
|
"""Parse [OK]/[FAIL] lines from status_common_services.sh output.
|
||||||
|
|
||||||
Returns a dict mapping component keys (argo, certmgr,
|
Returns a dict mapping component keys (registry, certmgr,
|
||||||
garage, kong, openbao, opentofu) to True (healthy) or False (unhealthy).
|
garage, kong, openbao, opentofu) to True (healthy) or False (unhealthy).
|
||||||
"""
|
"""
|
||||||
# Map resource names to traffic light component keys
|
# Map resource names to traffic light component keys
|
||||||
name_map = {
|
name_map = {
|
||||||
"argocd-server": "argo",
|
"registry": "registry",
|
||||||
"argocd-repo-server": "argo",
|
|
||||||
"argocd-dex-server": "argo",
|
|
||||||
"argocd-applicationset-controller": "argo",
|
|
||||||
"argocd-notifications-controller": "argo",
|
|
||||||
"argocd-redis": "argo",
|
|
||||||
"argocd-application-controller": "argo",
|
|
||||||
"opentofu": "opentofu",
|
"opentofu": "opentofu",
|
||||||
"garage": "garage",
|
"garage": "garage",
|
||||||
"openbao": "openbao",
|
"openbao": "openbao",
|
||||||
@ -1910,7 +1999,7 @@ class ClusterScreenMixin:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def _deploy_k3s_services(self):
|
def _deploy_k3s_services(self):
|
||||||
"""Deploy common services (ArgoCD/OpenTofu/Garage) to remote k3s cluster."""
|
"""Deploy common services (Registry/OpenTofu/Garage) to remote k3s cluster."""
|
||||||
|
|
||||||
def _deploy():
|
def _deploy():
|
||||||
console = self._k3s_service_deploy_console
|
console = self._k3s_service_deploy_console
|
||||||
|
|||||||
@ -8,13 +8,14 @@ import subprocess
|
|||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, messagebox, filedialog
|
from tkinter import ttk, messagebox, filedialog, simpledialog
|
||||||
from installer import screen as ui
|
from installer import screen as ui
|
||||||
from installer.config import get_docker_build_platform_args
|
from installer.config import get_docker_build_platform_args
|
||||||
from installer.core.env import (
|
from installer.core.env import (
|
||||||
PROJECT_ROOT,
|
PROJECT_ROOT,
|
||||||
_bool_str,
|
_bool_str,
|
||||||
_deployment_mode_from_env,
|
_deployment_mode_from_env,
|
||||||
|
_http_ping_registry,
|
||||||
_safe_str,
|
_safe_str,
|
||||||
_push_docker_image,
|
_push_docker_image,
|
||||||
get_resource_path,
|
get_resource_path,
|
||||||
@ -942,17 +943,43 @@ class DatabaseScreenMixin:
|
|||||||
"Building the prole-db Postgres image. This may take a few minutes.", y=200
|
"Building the prole-db Postgres image. This may take a few minutes.", y=200
|
||||||
)
|
)
|
||||||
|
|
||||||
# Local registry status (checked async)
|
# Registry status (checked async)
|
||||||
self._db_registry_status_label = ui.canvas_text(
|
self._db_registry_status_label = ui.canvas_text(
|
||||||
self,
|
self,
|
||||||
48,
|
48,
|
||||||
230,
|
230,
|
||||||
"Local Registry: Checking...",
|
"Registry: Checking...",
|
||||||
fill="#6e6e73",
|
fill="#6e6e73",
|
||||||
font=("SF Pro Text", 11),
|
font=("SF Pro Text", 11),
|
||||||
)
|
)
|
||||||
self._canvas_items.append(self._db_registry_status_label)
|
self._canvas_items.append(self._db_registry_status_label)
|
||||||
|
|
||||||
|
# k3s registry initialization (shown only when needed)
|
||||||
|
self._db_init_registry_button = tk.Button(
|
||||||
|
self.bg_canvas,
|
||||||
|
text="Initialize Registry",
|
||||||
|
command=self._db_init_registry,
|
||||||
|
bg="#F5F5DC",
|
||||||
|
fg="black",
|
||||||
|
activebackground="#E5E5D5",
|
||||||
|
highlightbackground="#F5F5DC",
|
||||||
|
highlightthickness=0,
|
||||||
|
relief="flat",
|
||||||
|
font=("SF Pro Text", 11),
|
||||||
|
padx=16,
|
||||||
|
pady=6,
|
||||||
|
)
|
||||||
|
init_btn_window = self.bg_canvas.create_window(
|
||||||
|
48, 224, window=self._db_init_registry_button, anchor="nw", width=200
|
||||||
|
)
|
||||||
|
self._db_init_registry_button_canvas_window = init_btn_window
|
||||||
|
self._canvas_items.append(init_btn_window)
|
||||||
|
self._overlay_widgets.append(self._db_init_registry_button)
|
||||||
|
try:
|
||||||
|
self.bg_canvas.itemconfigure(init_btn_window, state="hidden")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Output Console
|
# Output Console
|
||||||
self._db_build_console = self._create_console_output(
|
self._db_build_console = self._create_console_output(
|
||||||
y=260, title="Build Output", width=900, height=520
|
y=260, title="Build Output", width=900, height=520
|
||||||
@ -991,43 +1018,112 @@ class DatabaseScreenMixin:
|
|||||||
def _ensure_db_build_registry_async(self):
|
def _ensure_db_build_registry_async(self):
|
||||||
def worker():
|
def worker():
|
||||||
try:
|
try:
|
||||||
if not self.check_docker_running():
|
env_key = "dev"
|
||||||
|
try:
|
||||||
|
env_key = self._cluster_env_key()
|
||||||
|
except Exception:
|
||||||
|
env_key = "dev"
|
||||||
|
|
||||||
|
# If the user selected Service, treat it as k3s even when Global/DEPLOYMENT_MODE remains k3d.
|
||||||
|
mode = _deployment_mode_from_env(
|
||||||
|
((self.prole_cfg_data.get("Global", {}) or {}).get("DEPLOYMENT_MODE") or "").strip()
|
||||||
|
or env_key
|
||||||
|
)
|
||||||
|
|
||||||
|
def _show_init_button(show: bool):
|
||||||
|
try:
|
||||||
|
if not hasattr(self, "_db_init_registry_button_canvas_window"):
|
||||||
|
return
|
||||||
|
state = "normal" if show else "hidden"
|
||||||
|
self.bg_canvas.itemconfigure(
|
||||||
|
self._db_init_registry_button_canvas_window, state=state
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
# k3s: never warn about localhost registry. Instead offer init + show reachable status.
|
||||||
|
if mode == "k3s":
|
||||||
|
registry_url = ""
|
||||||
|
try:
|
||||||
|
registry_url = (self.ensure_registry_available(env_key) or "").strip()
|
||||||
|
except Exception:
|
||||||
|
registry_url = ""
|
||||||
|
|
||||||
|
reachable = False
|
||||||
|
if registry_url and ":" in registry_url:
|
||||||
|
host, port_s = registry_url.rsplit(":", 1)
|
||||||
|
try:
|
||||||
|
reachable = _http_ping_registry(host.strip(), int(port_s.strip()))
|
||||||
|
except Exception:
|
||||||
|
reachable = False
|
||||||
|
|
||||||
|
if registry_url and reachable:
|
||||||
self.safe_after(
|
self.safe_after(
|
||||||
lambda: (
|
lambda: (
|
||||||
|
_show_init_button(False),
|
||||||
self.bg_canvas.itemconfig(
|
self.bg_canvas.itemconfig(
|
||||||
self._db_registry_status_label,
|
self._db_registry_status_label,
|
||||||
text="Local Registry: Docker not running",
|
text=f"Registry: {registry_url}",
|
||||||
fill="#ff3b30",
|
fill="#34c759",
|
||||||
)
|
),
|
||||||
if self.bg_canvas.winfo_exists()
|
|
||||||
else None
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Not reachable / not configured -> prompt action
|
||||||
|
msg = (
|
||||||
|
f"Registry: {registry_url} (not initialized)"
|
||||||
|
if registry_url
|
||||||
|
else "Registry: not initialized"
|
||||||
|
)
|
||||||
|
self.safe_after(
|
||||||
|
lambda: (
|
||||||
|
_show_init_button(True),
|
||||||
|
self.bg_canvas.itemconfig(
|
||||||
|
self._db_registry_status_label,
|
||||||
|
text=msg,
|
||||||
|
fill="#ff9500",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Non-k3s: keep local registry checks (requires Docker)
|
||||||
|
if not self.check_docker_running():
|
||||||
|
self.safe_after(
|
||||||
|
lambda: (
|
||||||
|
_show_init_button(False),
|
||||||
|
self.bg_canvas.itemconfig(
|
||||||
|
self._db_registry_status_label,
|
||||||
|
text="Registry: Docker not running",
|
||||||
|
fill="#ff3b30",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
info = self.ensure_local_registry_available()
|
info = self.ensure_local_registry_available()
|
||||||
if info:
|
if info:
|
||||||
host_registry, _cluster_registry = info
|
host_registry, _cluster_registry = info
|
||||||
self.safe_after(
|
self.safe_after(
|
||||||
lambda: (
|
lambda: (
|
||||||
|
_show_init_button(False),
|
||||||
self.bg_canvas.itemconfig(
|
self.bg_canvas.itemconfig(
|
||||||
self._db_registry_status_label,
|
self._db_registry_status_label,
|
||||||
text=f"Local Registry: {host_registry}",
|
text=f"Registry: {host_registry}",
|
||||||
fill="#34c759",
|
fill="#34c759",
|
||||||
)
|
),
|
||||||
if self.bg_canvas.winfo_exists()
|
|
||||||
else None
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.safe_after(
|
self.safe_after(
|
||||||
lambda: (
|
lambda: (
|
||||||
|
_show_init_button(False),
|
||||||
self.bg_canvas.itemconfig(
|
self.bg_canvas.itemconfig(
|
||||||
self._db_registry_status_label,
|
self._db_registry_status_label,
|
||||||
text="Local Registry: unavailable",
|
text="Registry: unavailable",
|
||||||
fill="#ff3b30",
|
fill="#ff3b30",
|
||||||
)
|
),
|
||||||
if self.bg_canvas.winfo_exists()
|
|
||||||
else None
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
log_path = getattr(self, "_last_registry_log_path", None)
|
log_path = getattr(self, "_last_registry_log_path", None)
|
||||||
@ -1038,7 +1134,7 @@ class DatabaseScreenMixin:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = f"Local Registry: error ({e})"
|
msg = f"Registry: error ({e})"
|
||||||
self.safe_after(
|
self.safe_after(
|
||||||
lambda: (
|
lambda: (
|
||||||
self.bg_canvas.itemconfig(
|
self.bg_canvas.itemconfig(
|
||||||
@ -1058,6 +1154,181 @@ class DatabaseScreenMixin:
|
|||||||
|
|
||||||
threading.Thread(target=worker, daemon=True).start()
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
|
def _db_rescan_registry_status_after_init(self):
|
||||||
|
"""Re-scan registry availability after init.
|
||||||
|
|
||||||
|
Registry pods/services may take a few seconds to become reachable after
|
||||||
|
`etc/init_registry.sh` finishes. Schedule a few checks so the UI status
|
||||||
|
flips to green once ready.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
if getattr(self, "_db_registry_status_label", None) is not None:
|
||||||
|
self.safe_after(
|
||||||
|
lambda: self.bg_canvas.itemconfig(
|
||||||
|
self._db_registry_status_label,
|
||||||
|
text="Registry: Checking...",
|
||||||
|
fill="#6e6e73",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Progressive delays to allow the in-cluster registry to become ready.
|
||||||
|
for d in (0, 1000, 2000, 4000, 8000, 15000):
|
||||||
|
self.safe_after(lambda: self._ensure_db_build_registry_async(), delay=d)
|
||||||
|
|
||||||
|
def _db_init_registry(self):
|
||||||
|
"""Initialize the k3s in-cluster registry (registry:2) via etc/init_registry.sh."""
|
||||||
|
|
||||||
|
def _resolve_argocd_namespace() -> str:
|
||||||
|
ns = (
|
||||||
|
(self.prole_cfg_data.get("Global", {}) or {})
|
||||||
|
.get("ARGOCD_NAMESPACE", "")
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
if not ns:
|
||||||
|
ns = (os.environ.get("ARGOCD_NAMESPACE") or "").strip()
|
||||||
|
return ns or "argocd"
|
||||||
|
|
||||||
|
def _resolve_service_namespace_interactive() -> str:
|
||||||
|
# Prefer explicit UI value if available
|
||||||
|
ns = ""
|
||||||
|
try:
|
||||||
|
ns = (self.service_namespace.get() or "").strip()
|
||||||
|
except Exception:
|
||||||
|
ns = ""
|
||||||
|
if not ns:
|
||||||
|
ns = (
|
||||||
|
(self.prole_cfg_data.get("Global", {}) or {})
|
||||||
|
.get("SERVICE_NAMESPACE", "")
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
if not ns:
|
||||||
|
ns = (os.environ.get("SERVICE_NAMESPACE") or "").strip()
|
||||||
|
|
||||||
|
if ns:
|
||||||
|
return ns
|
||||||
|
|
||||||
|
# UI is interactive: ask the user.
|
||||||
|
try:
|
||||||
|
ns2 = simpledialog.askstring(
|
||||||
|
"Service Namespace",
|
||||||
|
"Enter the service namespace for the k3s registry:",
|
||||||
|
parent=self.root,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
ns2 = None
|
||||||
|
return (ns2 or "").strip()
|
||||||
|
|
||||||
|
def worker():
|
||||||
|
env_key = "service"
|
||||||
|
try:
|
||||||
|
env_key = self._cluster_env_key()
|
||||||
|
except Exception:
|
||||||
|
env_key = "service"
|
||||||
|
|
||||||
|
mode = _deployment_mode_from_env(env_key)
|
||||||
|
if mode != "k3s":
|
||||||
|
try:
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Registry",
|
||||||
|
"Registry initialization is only required for k3s (Service) mode.",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
service_ns = _resolve_service_namespace_interactive()
|
||||||
|
if not service_ns:
|
||||||
|
# In UI mode we must not guess; prompt is available above.
|
||||||
|
# Only fall back to Kubernetes 'default' when no input is available.
|
||||||
|
try:
|
||||||
|
if getattr(self, "root", None) is not None:
|
||||||
|
self.safe_after(
|
||||||
|
lambda: messagebox.showerror(
|
||||||
|
"Registry",
|
||||||
|
"Service namespace is required to initialize the registry.",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
service_ns = "default"
|
||||||
|
|
||||||
|
argocd_ns = _resolve_argocd_namespace()
|
||||||
|
|
||||||
|
script = PROJECT_ROOT / "etc" / "init_registry.sh"
|
||||||
|
cfg_path = getattr(self.controller, "cfg_path", None)
|
||||||
|
cmd = [str(script)]
|
||||||
|
if cfg_path:
|
||||||
|
cmd += ["--config", str(cfg_path)]
|
||||||
|
cmd += [
|
||||||
|
"--mode",
|
||||||
|
"k3s",
|
||||||
|
"-n",
|
||||||
|
argocd_ns,
|
||||||
|
"--registry-namespace",
|
||||||
|
service_ns,
|
||||||
|
"update",
|
||||||
|
]
|
||||||
|
|
||||||
|
self.safe_after(
|
||||||
|
lambda: (
|
||||||
|
self._db_init_registry_button.configure(state="disabled")
|
||||||
|
if self._db_init_registry_button.winfo_exists()
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if self._db_build_console:
|
||||||
|
self.safe_after(
|
||||||
|
lambda: self._db_build_console.write(
|
||||||
|
f"Running registry init: {' '.join(cmd)}\n\n"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
cwd=str(PROJECT_ROOT),
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
bufsize=1,
|
||||||
|
env=os.environ.copy(),
|
||||||
|
)
|
||||||
|
if proc.stdout:
|
||||||
|
for line in iter(proc.stdout.readline, ""):
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
if self._db_build_console:
|
||||||
|
self._db_build_console.write(line)
|
||||||
|
proc.wait()
|
||||||
|
rc = proc.returncode
|
||||||
|
except Exception as e:
|
||||||
|
rc = 1
|
||||||
|
if self._db_build_console:
|
||||||
|
self._db_build_console.write(f"Registry init failed: {e}\n")
|
||||||
|
|
||||||
|
if self._db_build_console:
|
||||||
|
self._db_build_console.write(
|
||||||
|
f"\nRegistry init {'completed' if rc == 0 else 'failed'} (exit {rc}).\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.safe_after(
|
||||||
|
lambda: (
|
||||||
|
self._db_init_registry_button.configure(state="normal")
|
||||||
|
if self._db_init_registry_button.winfo_exists()
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if rc == 0:
|
||||||
|
self._db_rescan_registry_status_after_init()
|
||||||
|
else:
|
||||||
|
self._ensure_db_build_registry_async()
|
||||||
|
|
||||||
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
def run_db_build(self):
|
def run_db_build(self):
|
||||||
self._action_flags["init_db_build.run_build"] = True
|
self._action_flags["init_db_build.run_build"] = True
|
||||||
|
|
||||||
@ -1289,14 +1560,96 @@ class DatabaseScreenMixin:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
# Service/prod clusters must pull from a registry; do not skip push.
|
||||||
|
registry_url = ""
|
||||||
|
try:
|
||||||
|
registry_url = (self.ensure_registry_available(env_key) or "").strip()
|
||||||
|
except Exception:
|
||||||
|
registry_url = ""
|
||||||
|
|
||||||
|
if not registry_url:
|
||||||
self._db_build_console.write(
|
self._db_build_console.write(
|
||||||
"Import skipped for non-dev clusters.\n"
|
"No registry configured/resolved for this environment. Initialize the registry first.\n"
|
||||||
)
|
)
|
||||||
self.safe_after(
|
self.safe_after(
|
||||||
lambda: (
|
lambda: (
|
||||||
self.bg_canvas.itemconfig(
|
self.bg_canvas.itemconfig(
|
||||||
self._db_build_status_label,
|
self._db_build_status_label,
|
||||||
text="Build complete. Import skipped.",
|
text="Build ok, registry not configured.",
|
||||||
|
fill="#ff3b30",
|
||||||
|
)
|
||||||
|
if self.bg_canvas.winfo_exists()
|
||||||
|
and self._db_build_status_label in self.bg_canvas.find_all()
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
remote_tag = f"{registry_url}/prole-db:{tag}"
|
||||||
|
self._db_build_console.write(
|
||||||
|
f"Tagging image for registry: {remote_tag}\n"
|
||||||
|
)
|
||||||
|
tag_res = subprocess.run(
|
||||||
|
["docker", "tag", image_name, remote_tag],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if tag_res.returncode != 0:
|
||||||
|
self._db_build_console.write(tag_res.stdout or "")
|
||||||
|
self._db_build_console.write(tag_res.stderr or "")
|
||||||
|
self._db_build_console.write("Tag failed; cannot push.\n")
|
||||||
|
self.safe_after(
|
||||||
|
lambda: (
|
||||||
|
self.bg_canvas.itemconfig(
|
||||||
|
self._db_build_status_label,
|
||||||
|
text="Build ok, tag failed.",
|
||||||
|
fill="#ff3b30",
|
||||||
|
)
|
||||||
|
if self.bg_canvas.winfo_exists()
|
||||||
|
and self._db_build_status_label in self.bg_canvas.find_all()
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.safe_after(
|
||||||
|
lambda: (
|
||||||
|
self.bg_canvas.itemconfig(
|
||||||
|
self._db_build_status_label,
|
||||||
|
text="Pushing to registry...",
|
||||||
|
fill="#34c759",
|
||||||
|
)
|
||||||
|
if self.bg_canvas.winfo_exists()
|
||||||
|
and self._db_build_status_label in self.bg_canvas.find_all()
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not _push_docker_image(
|
||||||
|
remote_tag, log_fn=self._db_build_console.write
|
||||||
|
):
|
||||||
|
self._db_build_console.write(
|
||||||
|
"Push failed. Ensure the registry is initialized and reachable.\n"
|
||||||
|
)
|
||||||
|
self.safe_after(
|
||||||
|
lambda: (
|
||||||
|
self.bg_canvas.itemconfig(
|
||||||
|
self._db_build_status_label,
|
||||||
|
text="Build ok, push failed.",
|
||||||
|
fill="#ff3b30",
|
||||||
|
)
|
||||||
|
if self.bg_canvas.winfo_exists()
|
||||||
|
and self._db_build_status_label in self.bg_canvas.find_all()
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._db_build_console.write(f"Push complete: {remote_tag}\n")
|
||||||
|
self.safe_after(
|
||||||
|
lambda: (
|
||||||
|
self.bg_canvas.itemconfig(
|
||||||
|
self._db_build_status_label,
|
||||||
|
text="Build, tag and push complete.",
|
||||||
fill="#34c759",
|
fill="#34c759",
|
||||||
)
|
)
|
||||||
if self.bg_canvas.winfo_exists()
|
if self.bg_canvas.winfo_exists()
|
||||||
|
|||||||
@ -176,9 +176,27 @@ class DockerScreenMixin:
|
|||||||
registry_container = f"k3d-{reg_name}"
|
registry_container = f"k3d-{reg_name}"
|
||||||
host_registry = "localhost:5000"
|
host_registry = "localhost:5000"
|
||||||
|
|
||||||
mode = _deployment_mode_from_env(
|
# Prefer the currently selected environment (UI) over the global config.
|
||||||
self.prole_cfg_data.get("Global", {}).get("DEPLOYMENT_MODE")
|
# This avoids incorrectly treating Service (k3s) as Dev (k3d) when
|
||||||
|
# Global/DEPLOYMENT_MODE is still set to k3d.
|
||||||
|
mode_source = ""
|
||||||
|
try:
|
||||||
|
mode_source = (self._cluster_env_key() or "").strip()
|
||||||
|
except Exception:
|
||||||
|
mode_source = ""
|
||||||
|
if not mode_source:
|
||||||
|
try:
|
||||||
|
mode_source = (
|
||||||
|
(self.prole_cfg_data.get("Global", {}) or {})
|
||||||
|
.get("DEPLOYMENT_MODE", "")
|
||||||
|
.strip()
|
||||||
)
|
)
|
||||||
|
except Exception:
|
||||||
|
mode_source = ""
|
||||||
|
if not mode_source:
|
||||||
|
mode_source = (mode_hint or "").strip()
|
||||||
|
|
||||||
|
mode = _deployment_mode_from_env(mode_source)
|
||||||
if mode == "k3s":
|
if mode == "k3s":
|
||||||
k3s_registry = self._k3s_registry_hostport()
|
k3s_registry = self._k3s_registry_hostport()
|
||||||
if not k3s_registry:
|
if not k3s_registry:
|
||||||
@ -344,134 +362,6 @@ class DockerScreenMixin:
|
|||||||
log_fp.close()
|
log_fp.close()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _collect_dependent_images(
|
|
||||||
self, include_supabase: bool, include_kerberos_proxy: bool
|
|
||||||
) -> list[str]:
|
|
||||||
images = set()
|
|
||||||
for rel_dir in ("k8s/prole", "k8s/openbao"):
|
|
||||||
base_dir = PROJECT_ROOT / rel_dir
|
|
||||||
if not base_dir.exists():
|
|
||||||
continue
|
|
||||||
images.update(_collect_images_from_files(list(base_dir.glob("*.yaml"))))
|
|
||||||
|
|
||||||
if include_supabase:
|
|
||||||
supa_home = _resolve_supabase_home(PROJECT_ROOT)
|
|
||||||
if supa_home:
|
|
||||||
docker_dir = supa_home / "docker"
|
|
||||||
compose_files = [docker_dir / "docker-compose.yml"]
|
|
||||||
if os.environ.get("SUPABASE_USE_DEV_COMPOSE") == "1":
|
|
||||||
compose_files.append(docker_dir / "dev" / "docker-compose.dev.yml")
|
|
||||||
images.update(_collect_images_from_files(compose_files))
|
|
||||||
|
|
||||||
if not include_supabase:
|
|
||||||
images = {img for img in images if "supabase" not in img}
|
|
||||||
|
|
||||||
if include_kerberos_proxy:
|
|
||||||
krb_img = os.environ.get("KRB5_AD_PROXY_IMAGE", "alpine/socat")
|
|
||||||
if krb_img:
|
|
||||||
images.add(krb_img)
|
|
||||||
|
|
||||||
return sorted(images)
|
|
||||||
|
|
||||||
def _prepull_images_to_registry(
|
|
||||||
self, include_supabase: bool, include_kerberos_proxy: bool, log=None
|
|
||||||
) -> bool:
|
|
||||||
def _log(msg: str):
|
|
||||||
if log:
|
|
||||||
try:
|
|
||||||
log(msg)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
info = self.ensure_local_registry_available()
|
|
||||||
if not info:
|
|
||||||
_log("Local registry unavailable; skipping image pre-pull.\n")
|
|
||||||
return False
|
|
||||||
|
|
||||||
registry, _cluster_registry = info
|
|
||||||
images = self._collect_dependent_images(
|
|
||||||
include_supabase, include_kerberos_proxy
|
|
||||||
)
|
|
||||||
if not images:
|
|
||||||
_log("No dependent images found to pre-pull.\n")
|
|
||||||
return True
|
|
||||||
|
|
||||||
overall_ok = True
|
|
||||||
import_dir = self.docker_import_dir.get().strip()
|
|
||||||
|
|
||||||
for image in images:
|
|
||||||
local_tag = image
|
|
||||||
if not image.startswith(f"{registry}/"):
|
|
||||||
local_tag = f"{registry}/{image}"
|
|
||||||
|
|
||||||
# 1. Check if already in local registry
|
|
||||||
_log(f"Checking if {image} exists in local registry...\n")
|
|
||||||
check_reg = subprocess.run(
|
|
||||||
["docker", "pull", local_tag], capture_output=True, text=True
|
|
||||||
)
|
|
||||||
if check_reg.returncode == 0:
|
|
||||||
_log(f"[OK] {image} already exists in local registry as {local_tag}\n")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 2. Check if we already have it in local docker daemon
|
|
||||||
check_local = subprocess.run(
|
|
||||||
["docker", "image", "inspect", image], capture_output=True, text=True
|
|
||||||
)
|
|
||||||
found = check_local.returncode == 0
|
|
||||||
|
|
||||||
if not found and import_dir and os.path.isdir(import_dir):
|
|
||||||
# 3. Check import directory
|
|
||||||
safe_name = image.replace("/", "_").replace(":", "_")
|
|
||||||
tar_path = Path(import_dir) / f"{safe_name}.tar"
|
|
||||||
if tar_path.exists():
|
|
||||||
_log(f"Found {tar_path} in import directory, loading...\n")
|
|
||||||
load = subprocess.run(
|
|
||||||
["docker", "load", "-i", str(tar_path)],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
if load.returncode == 0:
|
|
||||||
found = True
|
|
||||||
else:
|
|
||||||
_log(f"[WARN] Failed to load {tar_path}: {load.stderr}\n")
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
# 4. Pull from Docker Hub
|
|
||||||
_log(f"Pulling {image} from Docker Hub...\n")
|
|
||||||
pull = subprocess.run(
|
|
||||||
["docker", "pull", image], capture_output=True, text=True
|
|
||||||
)
|
|
||||||
if pull.returncode != 0:
|
|
||||||
overall_ok = False
|
|
||||||
_log(pull.stdout or "")
|
|
||||||
_log(pull.stderr or "")
|
|
||||||
_log(f"[ERROR] docker pull failed for {image}\n")
|
|
||||||
continue
|
|
||||||
found = True
|
|
||||||
|
|
||||||
# If we have the image locally, tag and push to local registry
|
|
||||||
if found:
|
|
||||||
if local_tag != image:
|
|
||||||
tag = subprocess.run(
|
|
||||||
["docker", "tag", image, local_tag],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
if tag.returncode != 0:
|
|
||||||
overall_ok = False
|
|
||||||
_log(tag.stdout or "")
|
|
||||||
_log(tag.stderr or "")
|
|
||||||
_log(f"[ERROR] docker tag failed for {image} to {local_tag}\n")
|
|
||||||
continue
|
|
||||||
|
|
||||||
_log(f"Pushing {local_tag} to local registry...\n")
|
|
||||||
if not _push_docker_image(local_tag, log_fn=log):
|
|
||||||
overall_ok = False
|
|
||||||
continue
|
|
||||||
_log(f"[OK] Stored {image} in local registry as {local_tag}\n")
|
|
||||||
|
|
||||||
return overall_ok
|
|
||||||
|
|
||||||
def build_docker_image(self):
|
def build_docker_image(self):
|
||||||
"""Build prole-db Docker image"""
|
"""Build prole-db Docker image"""
|
||||||
# If Kerberos is enabled, update pg_hba.conf in conf/postgresql before copying
|
# If Kerberos is enabled, update pg_hba.conf in conf/postgresql before copying
|
||||||
|
|||||||
@ -339,9 +339,12 @@ class NavigationMixin:
|
|||||||
if current_id == "kerberos_config":
|
if current_id == "kerberos_config":
|
||||||
self.show_page("init_scripts")
|
self.show_page("init_scripts")
|
||||||
return
|
return
|
||||||
if current_id == "gitops_config":
|
if current_id == "argocd_config":
|
||||||
self.show_page("kerberos_config")
|
self.show_page("kerberos_config")
|
||||||
return
|
return
|
||||||
|
if current_id == "gitops_config":
|
||||||
|
self.show_page("argocd_config")
|
||||||
|
return
|
||||||
if current_id == "supabase_config":
|
if current_id == "supabase_config":
|
||||||
self.show_page("gitops_config")
|
self.show_page("gitops_config")
|
||||||
return
|
return
|
||||||
@ -527,7 +530,7 @@ class NavigationMixin:
|
|||||||
if self.kerberos_enabled.get():
|
if self.kerberos_enabled.get():
|
||||||
self.show_page("kerberos_config")
|
self.show_page("kerberos_config")
|
||||||
else:
|
else:
|
||||||
self.show_page("gitops_config")
|
self.show_page("argocd_config")
|
||||||
return
|
return
|
||||||
|
|
||||||
if current_id == "kerberos_config":
|
if current_id == "kerberos_config":
|
||||||
@ -569,6 +572,29 @@ class NavigationMixin:
|
|||||||
os.environ.get("KRB5_AD_SERVICE_NAME", "prole-kerberos-ad-dc")
|
os.environ.get("KRB5_AD_SERVICE_NAME", "prole-kerberos-ad-dc")
|
||||||
)
|
)
|
||||||
self._save_prole_cfg()
|
self._save_prole_cfg()
|
||||||
|
self.show_page("argocd_config")
|
||||||
|
return
|
||||||
|
|
||||||
|
if current_id == "argocd_config":
|
||||||
|
self.prole_cfg_data["Optional Features"]["ARGOCD_ENABLED"] = str(
|
||||||
|
self.argocd_enabled.get()
|
||||||
|
)
|
||||||
|
argocd_status = (
|
||||||
|
"Skipped"
|
||||||
|
if not self.argocd_enabled.get()
|
||||||
|
else (
|
||||||
|
"Deployed"
|
||||||
|
if getattr(self, "_argocd_success", False)
|
||||||
|
else "Attempted"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
argocd_ns = (self.argocd_namespace.get() or "argocd").strip() or "argocd"
|
||||||
|
self.prole_cfg_data.setdefault("Global", {})["ARGOCD_NAMESPACE"] = argocd_ns
|
||||||
|
self.prole_cfg_data["ArgoCD"] = {
|
||||||
|
"STATUS": argocd_status,
|
||||||
|
"ARGOCD_NAMESPACE": argocd_ns,
|
||||||
|
}
|
||||||
|
self._save_prole_cfg()
|
||||||
self.show_page("gitops_config")
|
self.show_page("gitops_config")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -682,6 +708,14 @@ class NavigationMixin:
|
|||||||
self.next_button.configure(state="disabled")
|
self.next_button.configure(state="disabled")
|
||||||
else:
|
else:
|
||||||
self.next_button.configure(state="normal")
|
self.next_button.configure(state="normal")
|
||||||
|
elif pid == "argocd_config":
|
||||||
|
if self.argocd_enabled.get():
|
||||||
|
if getattr(self, "_argocd_success", False):
|
||||||
|
self.next_button.configure(state="normal")
|
||||||
|
else:
|
||||||
|
self.next_button.configure(state="disabled")
|
||||||
|
else:
|
||||||
|
self.next_button.configure(state="normal")
|
||||||
elif pid == "init_scripts":
|
elif pid == "init_scripts":
|
||||||
# Initialization Scripts: Next is disabled until success
|
# Initialization Scripts: Next is disabled until success
|
||||||
if getattr(self, "_scripts_success", False):
|
if getattr(self, "_scripts_success", False):
|
||||||
|
|||||||
@ -5,7 +5,7 @@ metadata:
|
|||||||
name: prole-db
|
name: prole-db
|
||||||
spec:
|
spec:
|
||||||
instances: 3
|
instances: 3
|
||||||
imageName: myrddin.prole.org:5000/prole-db:17.7-106
|
imageName: myrddin.prole.org:5000/prole-db:18-118
|
||||||
postgresUID: 100
|
postgresUID: 100
|
||||||
postgresGID: 101
|
postgresGID: 101
|
||||||
maxSyncReplicas: 1
|
maxSyncReplicas: 1
|
||||||
|
|||||||
@ -4,7 +4,7 @@ metadata:
|
|||||||
name: prole-db
|
name: prole-db
|
||||||
spec:
|
spec:
|
||||||
instances: 3
|
instances: 3
|
||||||
imageName: myrddin.prole.org:5000/prole-db:17.7-106
|
imageName: myrddin.prole.org:5000/prole-db:18-118
|
||||||
postgresUID: 100
|
postgresUID: 100
|
||||||
postgresGID: 101
|
postgresGID: 101
|
||||||
maxSyncReplicas: 1
|
maxSyncReplicas: 1
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -48,6 +48,9 @@ BACKUP_STATUS_TIMEOUT=${BACKUP_STATUS_TIMEOUT:-600}
|
|||||||
BACKUP_STATUS_INTERVAL=${BACKUP_STATUS_INTERVAL:-10}
|
BACKUP_STATUS_INTERVAL=${BACKUP_STATUS_INTERVAL:-10}
|
||||||
PLUGIN_READY_TIMEOUT=${PLUGIN_READY_TIMEOUT:-180}
|
PLUGIN_READY_TIMEOUT=${PLUGIN_READY_TIMEOUT:-180}
|
||||||
|
|
||||||
|
PLUGIN_SOCKET_DIR=${PLUGIN_SOCKET_DIR:-/plugins}
|
||||||
|
LAST_BACKUP_NAME=""
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<USAGE
|
cat <<USAGE
|
||||||
Usage: $0 [start|backup|status]
|
Usage: $0 [start|backup|status]
|
||||||
@ -243,8 +246,10 @@ remove_native_barman_config() {
|
|||||||
|
|
||||||
wait_for_plugin_ready() {
|
wait_for_plugin_ready() {
|
||||||
local start_time now plugin_names deployment_rows ns ready replicas
|
local start_time now plugin_names deployment_rows ns ready replicas
|
||||||
|
local plugin_deploy_ready pod socket_path
|
||||||
start_time=$(date +%s)
|
start_time=$(date +%s)
|
||||||
while true; do
|
while true; do
|
||||||
|
plugin_deploy_ready=0
|
||||||
plugin_names=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.spec.plugins[*].name}' 2>/dev/null || true)
|
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
|
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)
|
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)
|
||||||
@ -257,9 +262,18 @@ wait_for_plugin_ready() {
|
|||||||
ready=${ready:-0}
|
ready=${ready:-0}
|
||||||
replicas=${replicas:-0}
|
replicas=${replicas:-0}
|
||||||
if (( ready >= 1 && replicas >= 1 )); then
|
if (( ready >= 1 && replicas >= 1 )); then
|
||||||
return 0
|
plugin_deploy_ready=1
|
||||||
|
break
|
||||||
fi
|
fi
|
||||||
done <<< "$deployment_rows"
|
done <<< "$deployment_rows"
|
||||||
|
|
||||||
|
if (( plugin_deploy_ready == 1 )); then
|
||||||
|
pod=$(kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
||||||
|
socket_path="$PLUGIN_SOCKET_DIR/$BARMAN_PLUGIN_NAME"
|
||||||
|
if [[ -n "$pod" ]] && kubectl -n "$NAMESPACE" exec "$pod" -c postgres -- test -S "$socket_path" >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
now=$(date +%s)
|
now=$(date +%s)
|
||||||
@ -305,6 +319,21 @@ wait_for_successful_base_backup() {
|
|||||||
local start_time now elapsed
|
local start_time now elapsed
|
||||||
start_time=$(date +%s)
|
start_time=$(date +%s)
|
||||||
while true; do
|
while true; do
|
||||||
|
if [[ -n "$LAST_BACKUP_NAME" ]]; then
|
||||||
|
local phase err
|
||||||
|
phase=$(kubectl -n "$NAMESPACE" get backup "$LAST_BACKUP_NAME" -o jsonpath='{.status.phase}' 2>/dev/null || true)
|
||||||
|
err=$(kubectl -n "$NAMESPACE" get backup "$LAST_BACKUP_NAME" -o jsonpath='{.status.error}' 2>/dev/null || true)
|
||||||
|
case "${phase:-}" in
|
||||||
|
Completed|Succeeded|completed|succeeded)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
Failed|failed)
|
||||||
|
echo "ERROR: Backup '$LAST_BACKUP_NAME' failed: ${err:-<no error provided>}" >&2
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
if has_successful_base_backup; then
|
if has_successful_base_backup; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@ -324,6 +353,7 @@ trigger_backup() {
|
|||||||
local backup_type="${1:-full}"
|
local backup_type="${1:-full}"
|
||||||
local backup_name
|
local backup_name
|
||||||
backup_name="${CNPG_CLUSTER_NAME}-backup-$(date +%Y%m%d%H%M%S)"
|
backup_name="${CNPG_CLUSTER_NAME}-backup-$(date +%Y%m%d%H%M%S)"
|
||||||
|
LAST_BACKUP_NAME="$backup_name"
|
||||||
echo "Triggering ${backup_type} backup $backup_name ..."
|
echo "Triggering ${backup_type} backup $backup_name ..."
|
||||||
if [[ "$backup_type" == "incremental" || "$backup_type" == "incr" ]]; then
|
if [[ "$backup_type" == "incremental" || "$backup_type" == "incr" ]]; then
|
||||||
kubectl apply -n "$NAMESPACE" -f - <<BACKUP
|
kubectl apply -n "$NAMESPACE" -f - <<BACKUP
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
111
|
118
|
||||||
@ -8,7 +8,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from installer.core.controller import ProleController
|
from installer.core.controller import ProleController
|
||||||
from installer.core.env import _ensure_ansible_vault_credentials, PROJECT_ROOT
|
from installer.core.env import _ensure_ansible_vault_credentials, PROJECT_ROOT
|
||||||
from installer.core.actions import ProleSilentInstaller, _prepare_k3s_pipeline
|
from installer.core.actions import ProleConsoleInstaller, _prepare_k3s_pipeline
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@ -89,7 +89,7 @@ def main():
|
|||||||
|
|
||||||
if args.silent:
|
if args.silent:
|
||||||
_ensure_ansible_vault_credentials(prompt_ui=False)
|
_ensure_ansible_vault_credentials(prompt_ui=False)
|
||||||
installer = ProleSilentInstaller(
|
installer = ProleConsoleInstaller(
|
||||||
controller, args.config, reset_cluster=args.reset
|
controller, args.config, reset_cluster=args.reset
|
||||||
)
|
)
|
||||||
if args.log:
|
if args.log:
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import subprocess
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from installer import prole_conf
|
from installer import prole_conf
|
||||||
from installer.core.actions import ProleInstallerBase
|
from installer.core.actions import ProleInstaller
|
||||||
from installer.core.env import (
|
from installer.core.env import (
|
||||||
_detect_ansible_topology,
|
_detect_ansible_topology,
|
||||||
_sync_opentofu_pipeline,
|
_sync_opentofu_pipeline,
|
||||||
@ -39,7 +39,7 @@ _CFG_KEY_MAP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProleDeployment(ProleInstallerBase):
|
class ProleDeployment(ProleInstaller):
|
||||||
"""
|
"""
|
||||||
Top-level class for Prole infrastructure deployment via OpenTofu.
|
Top-level class for Prole infrastructure deployment via OpenTofu.
|
||||||
Duplicate k3d stack and apply to k3s defined in Ansible inventory.
|
Duplicate k3d stack and apply to k3s defined in Ansible inventory.
|
||||||
|
|||||||
@ -1,8 +1,15 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Run the Prole installer with execution instrumentation (coverage)
|
# Run the Prole installer with execution instrumentation (coverage)
|
||||||
# This helps identify unused code paths during manual testing/usage.
|
# This helps identify unused code paths during manual testing/usage.
|
||||||
export PYTHONPATH=$PYTHONPATH:.
|
|
||||||
coverage run install.py "$@"
|
set -e
|
||||||
coverage report -m
|
|
||||||
coverage html
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
echo "Execution instrumentation report generated in htmlcov/index.html"
|
PYTHON="$SCRIPT_DIR/bin/python"
|
||||||
|
|
||||||
|
export PYTHONPATH="$SCRIPT_DIR:${PYTHONPATH:-}"
|
||||||
|
|
||||||
|
"$PYTHON" -m coverage run "$SCRIPT_DIR/install.py" "$@"
|
||||||
|
"$PYTHON" -m coverage report -m
|
||||||
|
"$PYTHON" -m coverage html
|
||||||
|
echo "Execution instrumentation report generated in $SCRIPT_DIR/htmlcov/index.html"
|
||||||
|
|||||||
@ -125,6 +125,32 @@ get_ready_kdc_pod() {
|
|||||||
kubectl -n "$kdc_ns" get pod -l "app=${PROLE_KDC_NAME}" --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true
|
kubectl -n "$kdc_ns" get pod -l "app=${PROLE_KDC_NAME}" --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug_kdc_service_target() {
|
||||||
|
local host="${KRB5_KDC:-}"
|
||||||
|
if [[ -z "$host" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Only attempt Kubernetes Service diagnostics for in-cluster service FQDNs.
|
||||||
|
if [[ "$host" != *".svc.cluster.local" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local base svc ns
|
||||||
|
base="${host%.svc.cluster.local}"
|
||||||
|
svc="${base%%.*}"
|
||||||
|
ns="${base#*.}"
|
||||||
|
ns="${ns%%.*}"
|
||||||
|
|
||||||
|
if [[ -z "$svc" || -z "$ns" || "$svc" == "$base" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "--- KDC service diagnostics for ${svc}.${ns} ---" >&2
|
||||||
|
kubectl -n "$ns" get svc "$svc" -o wide 1>&2 || true
|
||||||
|
kubectl -n "$ns" get endpoints "$svc" -o yaml 1>&2 || true
|
||||||
|
}
|
||||||
|
|
||||||
wait_for_kdc_pod() {
|
wait_for_kdc_pod() {
|
||||||
local kdc_ns="$1"
|
local kdc_ns="$1"
|
||||||
local attempt pod
|
local attempt pod
|
||||||
@ -242,6 +268,15 @@ run_kinit() {
|
|||||||
else
|
else
|
||||||
echo "kinit produced no output." >&2
|
echo "kinit produced no output." >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "--- Debug: kinit failure context ---" >&2
|
||||||
|
echo "KRB5_REALM=${KRB5_REALM:-<unset>}" >&2
|
||||||
|
echo "KRB5_KDC=${KRB5_KDC:-<unset>}" >&2
|
||||||
|
echo "KRB5_CONFIG=${krb5_config}" >&2
|
||||||
|
# Print the relevant realm stanza (best-effort).
|
||||||
|
kubectl -n "$kdc_ns" exec "$pod_name" -- sh -c "grep -nE '^(\[realms\]|\[libdefaults\]|\[domain_realm\]|[[:space:]]*(kdc|admin_server)[[:space:]]*=)' '$krb5_config' || true" 1>&2 || true
|
||||||
|
debug_kdc_service_target
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -831,36 +831,12 @@ PYFIX
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# 9. Fix realtime liveness probe: kompose emits the whole curl command as a
|
# 9. Fix realtime liveness probe: kompose emits the whole curl command as a
|
||||||
# single quoted string instead of a proper argv list. Wrap it in sh -c.
|
# single argv entry, and unquoted `Authorization: ...` fragments can be
|
||||||
|
# parsed as YAML mappings. Sanitize the probe so Kubernetes receives a
|
||||||
|
# proper list of strings.
|
||||||
local realtime_deploy="$SUPABASE_K8S_DIR/realtime-deployment.yaml"
|
local realtime_deploy="$SUPABASE_K8S_DIR/realtime-deployment.yaml"
|
||||||
if [[ -f "$realtime_deploy" ]]; then
|
if [[ -f "$realtime_deploy" ]]; then
|
||||||
python3 - "$realtime_deploy" <<'PYFIX'
|
python3 "$SCRIPT_DIR/patch_realtime_probe.py" "$realtime_deploy"
|
||||||
import sys, re
|
|
||||||
|
|
||||||
path = sys.argv[1]
|
|
||||||
with open(path, 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# Match the single-string curl command in the exec probe
|
|
||||||
m = re.search(
|
|
||||||
r"(\s+command:\n)(\s+- )'(curl .+?)'",
|
|
||||||
content, re.DOTALL
|
|
||||||
)
|
|
||||||
if m:
|
|
||||||
indent = m.group(2)
|
|
||||||
curl_cmd = m.group(3)
|
|
||||||
replacement = (
|
|
||||||
m.group(1)
|
|
||||||
+ indent + "sh\n"
|
|
||||||
+ indent + "-c\n"
|
|
||||||
+ indent + curl_cmd
|
|
||||||
)
|
|
||||||
content = content[:m.start()] + replacement + content[m.end():]
|
|
||||||
|
|
||||||
with open(path, 'w') as f:
|
|
||||||
f.write(content)
|
|
||||||
print(" [OK] realtime liveness probe -> sh -c curl ...")
|
|
||||||
PYFIX
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -871,7 +847,8 @@ patch_supabase_credentials() {
|
|||||||
local jwt_secret=""
|
local jwt_secret=""
|
||||||
|
|
||||||
# Resolve postgres password from prole-db-superuser secret
|
# Resolve postgres password from prole-db-superuser secret
|
||||||
local ns="${PROLE_DB_NAMESPACE:-prole-db-a0001}"
|
resolve_prole_db_namespace
|
||||||
|
local ns="${PROLE_DB_NAMESPACE:-${NAMESPACE:-default}}"
|
||||||
if kubectl get secret prole-db-superuser -n "$ns" >/dev/null 2>&1; then
|
if kubectl get secret prole-db-superuser -n "$ns" >/dev/null 2>&1; then
|
||||||
pg_password=$(kubectl get secret prole-db-superuser -n "$ns" \
|
pg_password=$(kubectl get secret prole-db-superuser -n "$ns" \
|
||||||
-o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null || true)
|
-o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null || true)
|
||||||
@ -939,7 +916,8 @@ PYFIX
|
|||||||
setup_prole_db_for_supabase() {
|
setup_prole_db_for_supabase() {
|
||||||
log "Ensuring Supabase roles, schemas and databases exist in prole-db..."
|
log "Ensuring Supabase roles, schemas and databases exist in prole-db..."
|
||||||
|
|
||||||
local ns="${PROLE_DB_NAMESPACE:-prole-db-a0001}"
|
resolve_prole_db_namespace
|
||||||
|
local ns="${PROLE_DB_NAMESPACE:-${NAMESPACE:-default}}"
|
||||||
|
|
||||||
# Find the primary CNPG pod
|
# Find the primary CNPG pod
|
||||||
local primary
|
local primary
|
||||||
@ -1100,7 +1078,11 @@ run_helm() {
|
|||||||
kubectl cluster-info >/dev/null 2>&1 || return 1
|
kubectl cluster-info >/dev/null 2>&1 || return 1
|
||||||
|
|
||||||
if [[ "$FORCE" == "true" ]]; then
|
if [[ "$FORCE" == "true" ]]; then
|
||||||
kubectl delete namespace supabase --ignore-not-found --wait=false >/dev/null 2>&1 || true
|
# Helm stores release metadata as Secrets in the namespace; it will fail if
|
||||||
|
# the namespace is stuck in Terminating.
|
||||||
|
force_reset_supabase_namespace
|
||||||
|
else
|
||||||
|
ensure_supabase_namespace
|
||||||
fi
|
fi
|
||||||
|
|
||||||
helm_render_values
|
helm_render_values
|
||||||
@ -1140,10 +1122,18 @@ PY
|
|||||||
}
|
}
|
||||||
|
|
||||||
ensure_supabase_namespace() {
|
ensure_supabase_namespace() {
|
||||||
if ! kubectl get namespace supabase >/dev/null 2>&1; then
|
if kubectl get namespace supabase >/dev/null 2>&1; then
|
||||||
|
local phase
|
||||||
|
phase=$(kubectl get namespace supabase -o jsonpath='{.status.phase}' 2>/dev/null || true)
|
||||||
|
if [[ "${phase:-}" == "Terminating" ]]; then
|
||||||
|
warn "Namespace 'supabase' is Terminating; resetting it"
|
||||||
|
force_reset_supabase_namespace
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
log "Creating 'supabase' namespace..."
|
log "Creating 'supabase' namespace..."
|
||||||
kubectl create namespace supabase
|
kubectl create namespace supabase
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
force_reset_supabase_namespace() {
|
force_reset_supabase_namespace() {
|
||||||
|
|||||||
@ -29,13 +29,13 @@
|
|||||||
},
|
},
|
||||||
"secret": {
|
"secret": {
|
||||||
"db": {
|
"db": {
|
||||||
"password": "${OPENBAO:kv/prole/prole-db001/db#password}",
|
"password": "fr3styl3",
|
||||||
"database": "postgres"
|
"database": "postgres"
|
||||||
},
|
},
|
||||||
"jwt": {
|
"jwt": {
|
||||||
"secret": "4e53e1ecb98eb32ee83b890bea79e678e952c6ae237edf3f65f37bf6dd2262eb",
|
"secret": "4e53e1ecb98eb32ee83b890bea79e678e952c6ae237edf3f65f37bf6dd2262eb",
|
||||||
"anonKey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InByb2xlLXN1cGFiYXNlIiwiaWF0IjoxNzcyNDM0NjM4LCJleHAiOjIwODc3OTQ2Mzh9.Wg05U6ioVntunXskmJs3DXMc1i9-O7UBts2cnBwPCoI",
|
"anonKey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InByb2xlLXN1cGFiYXNlIiwiaWF0IjoxNzczNTM4MjkyLCJleHAiOjIwODg4OTgyOTJ9.PoUrud3YpPeuw_sR1PRgLdFatkqoi6hag8kRERh-bE0",
|
||||||
"serviceKey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoicHJvbGUtc3VwYWJhc2UiLCJpYXQiOjE3NzI0MzQ2MzgsImV4cCI6MjA4Nzc5NDYzOH0.xZ179fB412UFdgDxc8XUv2EQt-L1nGyhocll09u2u38"
|
"serviceKey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoicHJvbGUtc3VwYWJhc2UiLCJpYXQiOjE3NzM1MzgyOTIsImV4cCI6MjA4ODg5ODI5Mn0.rFJn8pV-MhsG1QdqUCVFdppU69aunUh6TPjGzluh5Ic"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ingress": {
|
"ingress": {
|
||||||
|
|||||||
79
supabase/patch_realtime_probe.py
Normal file
79
supabase/patch_realtime_probe.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def _quote_yaml_single(s: str) -> str:
|
||||||
|
# YAML single-quoted scalars escape a single quote by doubling it.
|
||||||
|
return "'" + s.replace("'", "''") + "'"
|
||||||
|
|
||||||
|
|
||||||
|
def patch_realtime_deployment_yaml(content: str) -> tuple[str, bool]:
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
# Case A: kompose sometimes emits a single-quoted curl command as a single argv entry.
|
||||||
|
# Wrap it into `sh -c '<curl ...>'`.
|
||||||
|
def _wrap_single_curl(m: re.Match[str]) -> str:
|
||||||
|
nonlocal changed
|
||||||
|
changed = True
|
||||||
|
cmd_hdr = m.group(1)
|
||||||
|
indent = m.group(2)
|
||||||
|
curl_cmd = m.group(3)
|
||||||
|
return (
|
||||||
|
cmd_hdr
|
||||||
|
+ indent
|
||||||
|
+ "sh\n"
|
||||||
|
+ indent
|
||||||
|
+ "-c\n"
|
||||||
|
+ indent
|
||||||
|
+ _quote_yaml_single(curl_cmd)
|
||||||
|
+ "\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
content = re.sub(
|
||||||
|
r"(\s+command:\n)(\s+- )'(curl[^\n]+)'\n",
|
||||||
|
_wrap_single_curl,
|
||||||
|
content,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Case B: unquoted scalars containing `Authorization: ...` can be parsed as YAML mappings.
|
||||||
|
# Quote them to guarantee Kubernetes receives a string.
|
||||||
|
out_lines: list[str] = []
|
||||||
|
for line in content.splitlines(True):
|
||||||
|
m = re.match(r"^(\s*-\s+)(curl\b.*)$", line)
|
||||||
|
if m:
|
||||||
|
prefix = m.group(1)
|
||||||
|
cmd = m.group(2).rstrip("\n")
|
||||||
|
if "Authorization:" in cmd and not cmd.lstrip().startswith(("'", '"')):
|
||||||
|
out_lines.append(prefix + _quote_yaml_single(cmd) + "\n")
|
||||||
|
changed = True
|
||||||
|
continue
|
||||||
|
out_lines.append(line)
|
||||||
|
|
||||||
|
return "".join(out_lines), changed
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print("Usage: patch_realtime_probe.py <path-to-realtime-deployment.yaml>", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
path = sys.argv[1]
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
patched, changed = patch_realtime_deployment_yaml(content)
|
||||||
|
if changed:
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(patched)
|
||||||
|
print(" [OK] realtime liveness probe YAML sanitized")
|
||||||
|
else:
|
||||||
|
print(" [OK] realtime liveness probe already sane")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
169
tests/etc/test_init_cloudnative_pg_bootstrap_admin_keys.sh
Normal file
169
tests/etc/test_init_cloudnative_pg_bootstrap_admin_keys.sh
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regression test: init_cloudnative_pg.sh should bootstrap-generate CNPG admin keys
|
||||||
|
# when OpenBao is unavailable and no local admin key files exist.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
PROLE_HOME=$(cd "$SCRIPT_DIR/../.." && pwd)
|
||||||
|
ETC_DIR="$PROLE_HOME/etc"
|
||||||
|
SCRIPT_UNDER_TEST="$ETC_DIR/init_cloudnative_pg.sh"
|
||||||
|
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
export TMP_DIR
|
||||||
|
|
||||||
|
BIN_DIR="$TMP_DIR/bin"
|
||||||
|
mkdir -p "$BIN_DIR"
|
||||||
|
|
||||||
|
mock_tool() {
|
||||||
|
cat <<M_EOF >"$BIN_DIR/$1"
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "Mocked $1 called with \$@" >>"$TMP_DIR/mock_calls.log"
|
||||||
|
exit 0
|
||||||
|
M_EOF
|
||||||
|
chmod +x "$BIN_DIR/$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Custom kubectl mock: return minimal outputs so initialize() doesn't hang.
|
||||||
|
cat <<'K_EOF' >"$BIN_DIR/kubectl"
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
_log_file="${TMP_DIR}/mock_calls.log"
|
||||||
|
echo "Mocked kubectl called with $@" >>"${_log_file}"
|
||||||
|
|
||||||
|
args="$*"
|
||||||
|
|
||||||
|
# API server readiness probes used by wait_for_apiserver_ready()
|
||||||
|
if [[ "${args}" == *"get --raw=/readyz"* || "${args}" == *"get --raw='/readyz'"* || "${args}" == *"get --raw=\"/readyz\""* ]]; then
|
||||||
|
echo "ok"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${args}" == *"version --short"* ]]; then
|
||||||
|
echo "Client Version: v0.0.0"
|
||||||
|
echo "Server Version: v0.0.0"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Simulate kubectl-cnpg plugin being present, but psql connectivity not yet ready.
|
||||||
|
if [[ "${args}" == *"cnpg"*"version"* ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "${args}" == *"cnpg"*"psql"* ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pretend required secrets exist (DB user + TLS artifacts)
|
||||||
|
if [[ "${args}" == *"get secret"*"prole-db-user"* ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "${args}" == *"get secret"*"prole-db-tls"* || "${args}" == *"get secret"*"prole-db-ca"* ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# CNPG webhook wait: return an endpoint IP so wait passes quickly.
|
||||||
|
if [[ "${args}" == *"get endpoints"*"cnpg-webhook-service"* ]]; then
|
||||||
|
echo "10.42.0.10"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# CNPG pods listing and readiness queries
|
||||||
|
if [[ "${args}" == *"get pods"* && "${args}" == *"cnpg.io/cluster="* ]]; then
|
||||||
|
if [[ "${args}" == *"--no-headers"* ]]; then
|
||||||
|
echo "prole-db-1 1/1 Running 0 1m"
|
||||||
|
echo "prole-db-2 1/1 Running 0 1m"
|
||||||
|
echo "prole-db-3 1/1 Running 0 1m"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${args}" == *"-o jsonpath="* ]]; then
|
||||||
|
printf "True\nTrue\nTrue\n"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cluster instances
|
||||||
|
if [[ "${args}" == *"get cluster"*"jsonpath="*".spec.instances"* ]]; then
|
||||||
|
echo "3"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For kubectl create secret --dry-run=client -o yaml, emit a minimal YAML so pipes look realistic.
|
||||||
|
if [[ "${args}" == *"create secret"*"--dry-run=client"*"-o yaml"* ]]; then
|
||||||
|
echo "apiVersion: v1"
|
||||||
|
echo "kind: Secret"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
K_EOF
|
||||||
|
chmod +x "$BIN_DIR/kubectl"
|
||||||
|
|
||||||
|
# Other tool mocks
|
||||||
|
mock_tool curl
|
||||||
|
mock_tool docker
|
||||||
|
mock_tool k3d
|
||||||
|
mock_tool skopeo
|
||||||
|
mock_tool jq
|
||||||
|
|
||||||
|
# Mock prole.cfg: point PROLE_SERVICE at a non-writable dir so the script must fallback to HOME
|
||||||
|
mkdir -p "$TMP_DIR/conf"
|
||||||
|
mkdir -p "$TMP_DIR/service"
|
||||||
|
chmod 0500 "$TMP_DIR/service" || true
|
||||||
|
|
||||||
|
TMP_HOME="$TMP_DIR/home"
|
||||||
|
mkdir -p "$TMP_HOME"
|
||||||
|
|
||||||
|
cat <<C_EOF >"$TMP_DIR/conf/prole.cfg"
|
||||||
|
[User]
|
||||||
|
NAMESPACE = test-ns
|
||||||
|
SERVICE_NAMESPACE = test-system
|
||||||
|
PROLE_HOME = $PROLE_HOME
|
||||||
|
PROLE_SERVICE = $TMP_DIR/service
|
||||||
|
|
||||||
|
[Global]
|
||||||
|
DEPLOYMENT_MODE = k3d
|
||||||
|
KUBECONTEXT = test
|
||||||
|
|
||||||
|
[Docker Build]
|
||||||
|
LOCAL_REGISTRY = localhost:5000
|
||||||
|
LOCAL_REGISTRY_INTERNAL = k3d-prole-registry.localhost:5000
|
||||||
|
C_EOF
|
||||||
|
|
||||||
|
export PATH="$BIN_DIR:$PATH"
|
||||||
|
export HOME="$TMP_HOME"
|
||||||
|
export PROLE_HOME="$PROLE_HOME"
|
||||||
|
export PROLE_CONF="$TMP_DIR/conf"
|
||||||
|
export PROLE_PASSWD="test-password"
|
||||||
|
export PROLE_SERVICE="$TMP_DIR/service"
|
||||||
|
export CNPG_WAIT_TIMEOUT=5
|
||||||
|
|
||||||
|
set +e
|
||||||
|
bash "$SCRIPT_UNDER_TEST" --mode k3d initialize >"$TMP_DIR/stdout" 2>"$TMP_DIR/stderr"
|
||||||
|
RC=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ $RC -ne 0 ]]; then
|
||||||
|
echo "FAILURE: init_cloudnative_pg.sh initialize returned rc=$RC"
|
||||||
|
echo "--- stdout ---"
|
||||||
|
sed -n '1,200p' "$TMP_DIR/stdout" || true
|
||||||
|
echo "--- stderr ---"
|
||||||
|
sed -n '1,200p' "$TMP_DIR/stderr" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SECRETS_DIR_FALLBACK="$TMP_HOME/.prole/etc/secrets"
|
||||||
|
if [[ ! -f "$SECRETS_DIR_FALLBACK/admin.key" || ! -f "$SECRETS_DIR_FALLBACK/admin.pub" ]]; then
|
||||||
|
echo "FAILURE: expected bootstrap-generated admin.key/admin.pub in fallback secrets dir: $SECRETS_DIR_FALLBACK"
|
||||||
|
ls -la "$SECRETS_DIR_FALLBACK" 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$SECRETS_DIR_FALLBACK/admin_ed25519.key" || ! -f "$SECRETS_DIR_FALLBACK/admin_ed25519.pub" ]]; then
|
||||||
|
echo "FAILURE: expected legacy-mirrored admin_ed25519.key/.pub in fallback secrets dir: $SECRETS_DIR_FALLBACK"
|
||||||
|
ls -la "$SECRETS_DIR_FALLBACK" 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "SUCCESS"
|
||||||
202
tests/etc/test_init_cnpg_backup_start_waits_for_plugin_socket.sh
Normal file
202
tests/etc/test_init_cnpg_backup_start_waits_for_plugin_socket.sh
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Unit test for etc/init_cnpg_backup.sh (start action)
|
||||||
|
# Verifies we do not trigger the first Backup until the plugin socket exists in a CNPG pod.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
PROLE_HOME_REPO=$(cd "$SCRIPT_DIR/../.." && pwd)
|
||||||
|
ETC_DIR="$PROLE_HOME_REPO/etc"
|
||||||
|
SCRIPT_UNDER_TEST="$ETC_DIR/init_cnpg_backup.sh"
|
||||||
|
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
export TMP_DIR
|
||||||
|
|
||||||
|
BIN_DIR="$TMP_DIR/bin"
|
||||||
|
mkdir -p "$BIN_DIR"
|
||||||
|
|
||||||
|
cat <<'K_EOF' >"$BIN_DIR/kubectl"
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
_log_file="${TMP_DIR}/mock_calls.log"
|
||||||
|
echo "Mocked kubectl called with $*" >>"${_log_file}"
|
||||||
|
|
||||||
|
args="$*"
|
||||||
|
|
||||||
|
# API server readiness probes
|
||||||
|
if [[ "${args}" == *"get --raw=/readyz"* || "${args}" == *"get --raw='/readyz'"* || "${args}" == *"get --raw=\"/readyz\""* ]]; then
|
||||||
|
echo "ok"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "${args}" == *"version --short"* ]]; then
|
||||||
|
echo "Client Version: v0.0.0"
|
||||||
|
echo "Server Version: v0.0.0"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Namespace existence/creation
|
||||||
|
if [[ "${args}" == "get namespace test-ns" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "${args}" == "create namespace test-ns" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cluster existence and plugin config presence
|
||||||
|
if [[ "${args}" == "get cluster prole-db -n test-ns"* ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "${args}" == *"-n test-ns"*"get cluster prole-db"*"-o jsonpath="*".spec.plugins"*".name"* ]]; then
|
||||||
|
echo "barman-cloud.cloudnative-pg.io"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "${args}" == *"-n test-ns"*"get cluster prole-db"*"-o jsonpath="*".spec.plugins"* ]]; then
|
||||||
|
echo '[{"enabled":true,"isWALArchiver":true,"name":"barman-cloud.cloudnative-pg.io","parameters":{"barmanObjectName":"prole-db-barman-objectstore"}}]'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Barman CRD present
|
||||||
|
if [[ "${args}" == "get crd objectstores.barmancloud.cnpg.io"* ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# CNPG operator webhook endpoints ready
|
||||||
|
if [[ "${args}" == *"-n cnpg-system"*"get endpoints"*"cnpg-webhook-service"* ]]; then
|
||||||
|
echo "10.42.0.10"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Barman-cloud deployment ready
|
||||||
|
if [[ "${args}" == "get deployment -A -l app.kubernetes.io/name=barman-cloud -o jsonpath="* ]]; then
|
||||||
|
printf 'cnpg-system\t1\t1\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# CNPG cluster pod selection for socket check
|
||||||
|
if [[ "${args}" == *"-n test-ns"*"get pods"*"-l cnpg.io/cluster=prole-db"*"-o jsonpath={.items[0].metadata.name"* ]]; then
|
||||||
|
echo "prole-db-1"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Plugin socket check: fail twice, succeed on the third call
|
||||||
|
if [[ "${args}" == *"-n test-ns"*"exec prole-db-1 -c postgres -- test -S /plugins/barman-cloud.cloudnative-pg.io"* ]]; then
|
||||||
|
cfile="${TMP_DIR}/socket_checks"
|
||||||
|
c=0
|
||||||
|
[[ -f "$cfile" ]] && c=$(cat "$cfile")
|
||||||
|
c=$((c + 1))
|
||||||
|
echo "$c" >"$cfile"
|
||||||
|
if (( c >= 3 )); then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Garage pod selection
|
||||||
|
if [[ "${args}" == *"get pods -n test-system -l app=garage"*"-o jsonpath={.items[0].metadata.name"* ]]; then
|
||||||
|
echo "garage-0"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Garage exec commands
|
||||||
|
if [[ "${args}" == *"exec -n test-system garage-0 -- /garage"*"layout show"* ]]; then
|
||||||
|
echo "Current cluster layout version: 1"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "${args}" == *"exec -n test-system garage-0 -- /garage"*"status"* ]]; then
|
||||||
|
echo "OK"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "${args}" == *"exec -n test-system garage-0 -- /garage"*"key info"*"--show-secret"* ]]; then
|
||||||
|
echo "Access key ID: TESTACCESSKEY"
|
||||||
|
echo "Secret access key: TESTSECRETKEY"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "${args}" == *"exec -n test-system garage-0 -- /garage"*"bucket info"* ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "${args}" == *"exec -n test-system garage-0 -- /garage"*"bucket allow"* ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Capture applied manifests (ObjectStore, Secret, Backup)
|
||||||
|
if [[ "${args}" == apply*"-n"*"test-ns"*"-f"*"-"* ]]; then
|
||||||
|
cat >"${TMP_DIR}/applied.$(date +%s%N).yaml"
|
||||||
|
echo "applied"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Simulate the triggered backup reaching Completed quickly
|
||||||
|
if [[ "${args}" == *"-n test-ns"*"get backup"*"-o jsonpath={.status.phase"* ]]; then
|
||||||
|
echo "Completed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "${args}" == *"-n test-ns"*"get backup"*"-o jsonpath={.status.error"* ]]; then
|
||||||
|
echo ""
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "${args}" == "get backup -n test-ns -o jsonpath="* ]]; then
|
||||||
|
# name\tphase\tmethod\tbackupType
|
||||||
|
printf 'prole-db-backup-XYZ\tCompleted\tplugin\t\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${args}" == "get backup -n test-ns"* ]]; then
|
||||||
|
echo "prole-db-backup-XYZ Completed prole-db plugin"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
K_EOF
|
||||||
|
chmod +x "$BIN_DIR/kubectl"
|
||||||
|
|
||||||
|
export PATH="$BIN_DIR:$PATH"
|
||||||
|
|
||||||
|
# prole_cfg.sh sources $PROLE_HOME/env.sh if present; provide a minimal env.sh that points to our temp config.
|
||||||
|
CONF_DIR="$TMP_DIR/conf"
|
||||||
|
ENV_HOME="$TMP_DIR/env-home"
|
||||||
|
mkdir -p "$CONF_DIR" "$ENV_HOME"
|
||||||
|
|
||||||
|
cat <<C_EOF >"$CONF_DIR/prole.cfg"
|
||||||
|
[User]
|
||||||
|
NAMESPACE = test-ns
|
||||||
|
SERVICE_NAMESPACE = test-system
|
||||||
|
PROLE_HOME = $PROLE_HOME_REPO
|
||||||
|
PROLE_SERVICE = $TMP_DIR/service
|
||||||
|
|
||||||
|
[Global]
|
||||||
|
DEPLOYMENT_MODE = k3d
|
||||||
|
C_EOF
|
||||||
|
|
||||||
|
cat <<E_EOF >"$ENV_HOME/env.sh"
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
export PROLE_HOME="$PROLE_HOME_REPO"
|
||||||
|
export PROLE_CONF="$CONF_DIR"
|
||||||
|
E_EOF
|
||||||
|
chmod +x "$ENV_HOME/env.sh"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
PROLE_HOME="$ENV_HOME" bash "$SCRIPT_UNDER_TEST" start >"$TMP_DIR/stdout" 2>"$TMP_DIR/stderr"
|
||||||
|
RC=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ $RC -ne 0 ]]; then
|
||||||
|
echo "FAILURE: init_cnpg_backup.sh start returned rc=$RC"
|
||||||
|
sed -n '1,200p' "$TMP_DIR/mock_calls.log" || true
|
||||||
|
sed -n '1,200p' "$TMP_DIR/stderr" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure socket checks happened before any Backup apply
|
||||||
|
socket_line=$(grep -n "exec prole-db-1 -c postgres -- test -S /plugins/barman-cloud.cloudnative-pg.io" "$TMP_DIR/mock_calls.log" | tail -n1 | cut -d: -f1 || true)
|
||||||
|
backup_apply_line=$(grep -n "apply -n test-ns -f -" "$TMP_DIR/mock_calls.log" | tail -n1 | cut -d: -f1 || true)
|
||||||
|
|
||||||
|
if [[ -z "$socket_line" || -z "$backup_apply_line" || "$socket_line" -ge "$backup_apply_line" ]]; then
|
||||||
|
echo "FAILURE: expected plugin socket check before triggering backup apply"
|
||||||
|
echo "socket_line=$socket_line backup_apply_line=$backup_apply_line"
|
||||||
|
sed -n '1,220p' "$TMP_DIR/mock_calls.log" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "SUCCESS"
|
||||||
76
tests/etc/test_prole_cfg_kube_context_k3d_prefix.sh
Normal file
76
tests/etc/test_prole_cfg_kube_context_k3d_prefix.sh
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
PROLE_HOME=$(cd "$SCRIPT_DIR/../.." && pwd)
|
||||||
|
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
export TMP_DIR
|
||||||
|
|
||||||
|
BIN_DIR="$TMP_DIR/bin"
|
||||||
|
mkdir -p "$BIN_DIR"
|
||||||
|
|
||||||
|
cat <<'K_EOF' >"$BIN_DIR/kubectl"
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
args="$*"
|
||||||
|
|
||||||
|
if [[ "$args" == "config current-context"* ]]; then
|
||||||
|
echo "k3d-knoe-dev-cluster"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$args" == "config get-contexts -o name"* ]]; then
|
||||||
|
echo "docker-desktop"
|
||||||
|
echo "k3d-knoe-dev-cluster"
|
||||||
|
echo "k3d-knoe-system"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$args" == "config get-contexts --no-headers"* ]]; then
|
||||||
|
# Format: CURRENT NAME CLUSTER AUTHINFO NAMESPACE
|
||||||
|
echo "* k3d-knoe-dev-cluster k3d-knoe-dev-cluster user "
|
||||||
|
echo " k3d-knoe-system k3d-knoe-system user "
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$args" == "config use-context "* ]]; then
|
||||||
|
# shellcheck disable=SC2129
|
||||||
|
echo "${args#config use-context }" >"${TMP_DIR}/used_context"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
K_EOF
|
||||||
|
chmod +x "$BIN_DIR/kubectl"
|
||||||
|
|
||||||
|
mkdir -p "$TMP_DIR/conf"
|
||||||
|
cat <<C_EOF >"$TMP_DIR/conf/prole.cfg"
|
||||||
|
[Global]
|
||||||
|
DEPLOYMENT_MODE = k3d
|
||||||
|
KUBECONTEXT = knoe-system
|
||||||
|
C_EOF
|
||||||
|
|
||||||
|
export PATH="$BIN_DIR:$PATH"
|
||||||
|
export HOME="$TMP_DIR/home"
|
||||||
|
mkdir -p "$HOME"
|
||||||
|
|
||||||
|
export PROLE_HOME="$PROLE_HOME"
|
||||||
|
export PROLE_CONF="$TMP_DIR/conf"
|
||||||
|
export PROLE_MODE="k3d"
|
||||||
|
export KUBECONTEXT="knoe-system"
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$PROLE_HOME/etc/prole_cfg.sh"
|
||||||
|
|
||||||
|
prole_ensure_kube_context
|
||||||
|
|
||||||
|
if [[ "$(cat "$TMP_DIR/used_context")" != "k3d-knoe-system" ]]; then
|
||||||
|
echo "FAILURE: expected context 'k3d-knoe-system' but got '$(cat "$TMP_DIR/used_context")'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "SUCCESS"
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
PROLE_HOME=$(cd "$SCRIPT_DIR/../.." && pwd)
|
||||||
|
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
export TMP_DIR
|
||||||
|
|
||||||
|
BIN_DIR="$TMP_DIR/bin"
|
||||||
|
mkdir -p "$BIN_DIR"
|
||||||
|
|
||||||
|
cat <<'K_EOF' >"$BIN_DIR/kubectl"
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
args="$*"
|
||||||
|
|
||||||
|
if [[ "$args" == "config current-context"* ]]; then
|
||||||
|
echo "default"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$args" == "config get-contexts -o name"* ]]; then
|
||||||
|
echo "default"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$args" == "config get-contexts --no-headers"* ]]; then
|
||||||
|
# Format: CURRENT NAME CLUSTER AUTHINFO NAMESPACE
|
||||||
|
echo "* default default user "
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$args" == "config use-context "* ]]; then
|
||||||
|
ctx="${args#config use-context }"
|
||||||
|
if [[ "$ctx" == "default" ]]; then
|
||||||
|
echo "$ctx" >"${TMP_DIR}/used_context"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
K_EOF
|
||||||
|
chmod +x "$BIN_DIR/kubectl"
|
||||||
|
|
||||||
|
export PATH="$BIN_DIR:$PATH"
|
||||||
|
export HOME="$TMP_DIR/home"
|
||||||
|
mkdir -p "$HOME"
|
||||||
|
|
||||||
|
# Prevent prole_cfg.sh from trying to auto-discover kubeconfig during sourcing.
|
||||||
|
export KUBECONFIG="$TMP_DIR/kubeconfig"
|
||||||
|
|
||||||
|
export PROLE_HOME="$PROLE_HOME"
|
||||||
|
export PROLE_MODE="k3s"
|
||||||
|
export KUBECONTEXT="prole-k3s"
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$PROLE_HOME/etc/prole_cfg.sh"
|
||||||
|
|
||||||
|
prole_ensure_kube_context
|
||||||
|
|
||||||
|
if [[ "$(cat "$TMP_DIR/used_context")" != "default" ]]; then
|
||||||
|
echo "FAILURE: expected context 'default' but got '$(cat "$TMP_DIR/used_context")'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "SUCCESS"
|
||||||
112
tests/etc/test_status_k3d_common_services_namespace.sh
Normal file
112
tests/etc/test_status_k3d_common_services_namespace.sh
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regression test for etc/status.sh
|
||||||
|
# - In k3d mode, it must not auto-select prole-k3s.kubeconfig via the fallback.
|
||||||
|
# - It must call status_common_services.sh using SERVICE_NAMESPACE (not DB NAMESPACE).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
PROLE_HOME_REPO=$(cd "$SCRIPT_DIR/../.." && pwd)
|
||||||
|
ETC_DIR="$PROLE_HOME_REPO/etc"
|
||||||
|
SCRIPT_UNDER_TEST="$ETC_DIR/status.sh"
|
||||||
|
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
export TMP_DIR
|
||||||
|
export PROLE_K3S_KUBECONFIG_PATH="$PROLE_HOME_REPO/prole-k3s.kubeconfig"
|
||||||
|
|
||||||
|
BIN_DIR="$TMP_DIR/bin"
|
||||||
|
mkdir -p "$BIN_DIR"
|
||||||
|
|
||||||
|
cat <<'K_EOF' >"$BIN_DIR/kubectl"
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
_log_file="${TMP_DIR}/mock_calls.log"
|
||||||
|
echo "Mocked kubectl called with $*" >>"${_log_file}"
|
||||||
|
|
||||||
|
args="$*"
|
||||||
|
|
||||||
|
if [[ "$args" == "config current-context"* ]]; then
|
||||||
|
echo "k3d-knoe-system"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$args" == "config view --minify"* ]]; then
|
||||||
|
echo "https://127.0.0.1:6443"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$args" == "cluster-info"* ]]; then
|
||||||
|
if [[ "${KUBECONFIG:-}" == "${PROLE_K3S_KUBECONFIG_PATH}" ]]; then
|
||||||
|
echo "fallback-probe" >>"${TMP_DIR}/k3s_kubeconfig_probe"
|
||||||
|
fi
|
||||||
|
echo "Kubernetes control plane is running"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Basic pods output so status.sh can iterate.
|
||||||
|
if [[ "$args" == "get pods -o wide -A"* ]]; then
|
||||||
|
echo "NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE"
|
||||||
|
echo "kube-system coredns 1/1 Running 0 1h 10.0.0.1 node"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Common services checks (SERVICE_NAMESPACE should be svc-ns)
|
||||||
|
if [[ "$args" == "-n svc-ns get svc"* ]]; then
|
||||||
|
echo "NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE"
|
||||||
|
echo "openbao ClusterIP 10.0.0.2 <none> 8200/TCP 1h"
|
||||||
|
echo "garage ClusterIP 10.0.0.3 <none> 3900/TCP 1h"
|
||||||
|
echo "opentofu ClusterIP 10.0.0.4 <none> 8080/TCP 1h"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "$args" == "-n svc-ns get svc auth"* ]]; then
|
||||||
|
echo "NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE"
|
||||||
|
echo "auth ClusterIP 10.0.0.5 <none> 88/TCP 1h"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "$args" == "-n svc-ns get deploy"* || "$args" == "-n svc-ns get statefulset"* ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
K_EOF
|
||||||
|
chmod +x "$BIN_DIR/kubectl"
|
||||||
|
|
||||||
|
export PATH="$BIN_DIR:$PATH"
|
||||||
|
|
||||||
|
# Provide a reachable prole-k3s.kubeconfig file (fallback must NOT probe it in k3d mode)
|
||||||
|
touch "$PROLE_HOME_REPO/prole-k3s.kubeconfig"
|
||||||
|
|
||||||
|
CFG_DIR="$TMP_DIR/conf"
|
||||||
|
mkdir -p "$CFG_DIR"
|
||||||
|
|
||||||
|
cat <<C_EOF >"$CFG_DIR/prole.cfg"
|
||||||
|
[User]
|
||||||
|
NAMESPACE = db-ns
|
||||||
|
SERVICE_NAMESPACE = svc-ns
|
||||||
|
|
||||||
|
[Global]
|
||||||
|
DEPLOYMENT_MODE = k3d
|
||||||
|
C_EOF
|
||||||
|
|
||||||
|
set +e
|
||||||
|
STATUS_CHECK_TIMEOUT=2 bash "$SCRIPT_UNDER_TEST" -c "$CFG_DIR/prole.cfg" >"$TMP_DIR/stdout" 2>"$TMP_DIR/stderr"
|
||||||
|
RC=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# We don't require rc=0; we only assert the correct calls were made.
|
||||||
|
|
||||||
|
if [[ -f "$TMP_DIR/k3s_kubeconfig_probe" ]]; then
|
||||||
|
echo "FAILURE: expected status.sh not to probe prole-k3s.kubeconfig via kubectl cluster-info in k3d mode" >&2
|
||||||
|
sed -n '1,200p' "$TMP_DIR/mock_calls.log" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -q -- "-n svc-ns get svc opentofu garage openbao" "$TMP_DIR/mock_calls.log"; then
|
||||||
|
echo "FAILURE: expected status_common_services.sh checks to use SERVICE_NAMESPACE (svc-ns)" >&2
|
||||||
|
sed -n '1,200p' "$TMP_DIR/mock_calls.log" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "SUCCESS"
|
||||||
60
tests/etc/test_supabase_patch_realtime_probe.sh
Normal file
60
tests/etc/test_supabase_patch_realtime_probe.sh
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
PROLE_HOME=$(cd "$SCRIPT_DIR/../.." && pwd)
|
||||||
|
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
|
||||||
|
YAML_PATH="$TMP_DIR/realtime-deployment.yaml"
|
||||||
|
|
||||||
|
cat >"$YAML_PATH" <<'YAML'
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: realtime
|
||||||
|
namespace: supabase
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: realtime
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: realtime
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: realtime
|
||||||
|
image: supabase/realtime:v2.76.5
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- curl -sSfL --head -o /dev/null -H "Authorization: Bearer abc:def" http://localhost:4000/health
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 30
|
||||||
|
YAML
|
||||||
|
|
||||||
|
python3 "$PROLE_HOME/supabase/patch_realtime_probe.py" "$YAML_PATH" >/dev/null
|
||||||
|
|
||||||
|
# Validate that the curl command is YAML-quoted.
|
||||||
|
# (We avoid using `kubectl ... -o json` here because it may contact the cluster for discovery
|
||||||
|
# even in client dry-run mode, which makes the test flaky/offline-hostile.)
|
||||||
|
|
||||||
|
if ! grep -Eq "^[[:space:]]*-[[:space:]]*'curl .*Authorization: Bearer" "$YAML_PATH"; then
|
||||||
|
echo "FAILURE: expected the realtime liveness curl command to be YAML single-quoted" >&2
|
||||||
|
sed -n '1,120p' "$YAML_PATH" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -Eq "^[[:space:]]*-[[:space:]]*curl .*Authorization: Bearer" "$YAML_PATH"; then
|
||||||
|
echo "FAILURE: found an unquoted realtime liveness curl command (YAML may parse it as a mapping)" >&2
|
||||||
|
sed -n '1,120p' "$YAML_PATH" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "SUCCESS"
|
||||||
@ -11,8 +11,8 @@ from unittest.mock import MagicMock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from installer.core.actions import (
|
from installer.core.actions import (
|
||||||
ProleInstallerBase,
|
ProleInstaller,
|
||||||
ProleSilentInstaller,
|
ProleConsoleInstaller,
|
||||||
_configure_unbuffered_io,
|
_configure_unbuffered_io,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ from installer.core.actions import (
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class _TestableInstaller(ProleInstallerBase):
|
class _TestableInstaller(ProleInstaller):
|
||||||
"""Minimal concrete subclass for testing base-class helpers."""
|
"""Minimal concrete subclass for testing base-class helpers."""
|
||||||
|
|
||||||
def __init__(self, inputs=None, project_root=None):
|
def __init__(self, inputs=None, project_root=None):
|
||||||
@ -268,13 +268,16 @@ spec:
|
|||||||
|
|
||||||
inst = _TestableInstaller(project_root=tmp_path)
|
inst = _TestableInstaller(project_root=tmp_path)
|
||||||
|
|
||||||
|
with mock.patch.dict(
|
||||||
|
os.environ, {"KRB5_AD_PROXY_IMAGE": "krb-proxy:test"}, clear=False
|
||||||
|
):
|
||||||
images = inst._collect_dependent_images(
|
images = inst._collect_dependent_images(
|
||||||
include_supabase=True,
|
include_supabase=True,
|
||||||
include_kerberos_proxy=True,
|
include_kerberos_proxy=True,
|
||||||
)
|
)
|
||||||
assert "nginx:1.2" in images
|
assert "nginx:1.2" in images
|
||||||
assert "supabase/postgres:15" in images
|
assert "supabase/postgres:15" in images
|
||||||
assert "ghcr.io/bsharp-tech/prole-kerberos-proxy:latest" in images
|
assert "krb-proxy:test" in images
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -282,8 +285,8 @@ spec:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class _TestableSilentInstaller(ProleSilentInstaller):
|
class _TestableSilentInstaller(ProleConsoleInstaller):
|
||||||
"""Testable subclass of ProleSilentInstaller."""
|
"""Testable subclass of ProleConsoleInstaller."""
|
||||||
|
|
||||||
def __init__(self, inputs=None, project_root=None):
|
def __init__(self, inputs=None, project_root=None):
|
||||||
self._inputs_dict = inputs or {}
|
self._inputs_dict = inputs or {}
|
||||||
|
|||||||
48
tests/installer/test_argocd_canvas_only_page.py
Normal file
48
tests/installer/test_argocd_canvas_only_page.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_page_argocd_config_hides_slide_area():
|
||||||
|
"""Regression: `argocd_config` is a canvas-rendered page and must not be covered by `slide_area`."""
|
||||||
|
|
||||||
|
from installer.ui.screens.base import ScreenBaseMixin
|
||||||
|
|
||||||
|
class _SlideArea:
|
||||||
|
def __init__(self):
|
||||||
|
self.place_forget_called = 0
|
||||||
|
self.place_called = 0
|
||||||
|
|
||||||
|
def place_forget(self):
|
||||||
|
self.place_forget_called += 1
|
||||||
|
|
||||||
|
def place(self, **_kwargs):
|
||||||
|
self.place_called += 1
|
||||||
|
|
||||||
|
def lift(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def configure(self, **_kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
class _Dummy(ScreenBaseMixin):
|
||||||
|
def __init__(self):
|
||||||
|
self._showing_service_overlay = False
|
||||||
|
self.page_index = 0
|
||||||
|
self.pages = [("argocd_config", None)]
|
||||||
|
self.page_frames = {}
|
||||||
|
self.canvas_renderers = {"argocd_config": lambda: None}
|
||||||
|
self.slide_area = _SlideArea()
|
||||||
|
|
||||||
|
def _clear_canvas_page(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _update_nav_highlight(self, _pid: str):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update_footer(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
s = _Dummy()
|
||||||
|
s.show_page("argocd_config")
|
||||||
|
|
||||||
|
assert s.slide_area.place_forget_called == 1
|
||||||
|
assert s.slide_area.place_called == 0
|
||||||
92
tests/installer/test_cluster_save_triggers_status_check.py
Normal file
92
tests/installer/test_cluster_save_triggers_status_check.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import sys
|
||||||
|
import tkinter as tk
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# Ensure `installer` is importable when tests are invoked directly via `pytest`
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
if str(PROJECT_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
import installer.ui.screens.cluster as cluster_mod
|
||||||
|
from installer.ui.screens.base import ScreenBaseMixin
|
||||||
|
from installer.ui.screens.cluster import ClusterScreenMixin
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyClusterSaveApp(ScreenBaseMixin, ClusterScreenMixin):
|
||||||
|
"""Minimal surface to exercise the Cluster Environment screen Save behavior."""
|
||||||
|
|
||||||
|
def __init__(self, root: tk.Tk):
|
||||||
|
self.root = root
|
||||||
|
# Canvas is not required by `_validate_and_save_cluster_config`, but other mixins
|
||||||
|
# assume it exists.
|
||||||
|
self.bg_canvas = tk.Canvas(root, width=1, height=1)
|
||||||
|
self._canvas_items = []
|
||||||
|
self._overlay_widgets = []
|
||||||
|
|
||||||
|
self.cluster_env = tk.StringVar(master=root, value="service")
|
||||||
|
self.selected_k3d_cluster = tk.StringVar(master=root, value="")
|
||||||
|
self.selected_kubectx = tk.StringVar(master=root, value="")
|
||||||
|
self.service_namespace = tk.StringVar(master=root, value="knoe-system")
|
||||||
|
self.prod_artifacts_path = tk.StringVar(master=root, value=str(Path.home()))
|
||||||
|
|
||||||
|
self.k3s_server_url = tk.StringVar(master=root, value="")
|
||||||
|
self.k3s_token = tk.StringVar(master=root, value="")
|
||||||
|
|
||||||
|
self.supabase_enabled = tk.BooleanVar(master=root, value=False)
|
||||||
|
self.gitops_enabled = tk.BooleanVar(master=root, value=False)
|
||||||
|
self.kerberos_enabled = tk.BooleanVar(master=root, value=False)
|
||||||
|
self.at_rest_encryption_enabled = tk.BooleanVar(master=root, value=False)
|
||||||
|
|
||||||
|
# Minimal config structure required by `_validate_and_save_cluster_config`.
|
||||||
|
self.prole_cfg_data = {
|
||||||
|
"Initialize Cluster": {
|
||||||
|
"ENVIRONMENT": "service",
|
||||||
|
"K3S_SERVER_URL": "",
|
||||||
|
"K3S_TOKEN": "",
|
||||||
|
},
|
||||||
|
"Global": {
|
||||||
|
# Keep old/new equal so we don't trigger the namespace cleanup prompt.
|
||||||
|
"SERVICE_NAMESPACE": "knoe-system",
|
||||||
|
},
|
||||||
|
"Optional Features": {},
|
||||||
|
"Prod Cluster (k8s)": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
self.saved_calls = 0
|
||||||
|
self.status_check_calls = 0
|
||||||
|
|
||||||
|
def _save_prole_cfg(self):
|
||||||
|
self.saved_calls += 1
|
||||||
|
|
||||||
|
def _verify_k3s_services(self):
|
||||||
|
self.status_check_calls += 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_cluster_env_save_triggers_status_check(monkeypatch):
|
||||||
|
# Some test modules replace tkinter with MagicMocks at import-time.
|
||||||
|
if isinstance(sys.modules.get("tkinter"), MagicMock):
|
||||||
|
pytest.skip("tkinter is mocked in this test run")
|
||||||
|
|
||||||
|
# Avoid depending on a real kubeconfig file.
|
||||||
|
monkeypatch.setattr(cluster_mod, "_find_kubeconfig_file", lambda: "/tmp/kubeconfig")
|
||||||
|
# Avoid any real secret handling.
|
||||||
|
monkeypatch.setattr(cluster_mod, "_encrypt_cfg_secret", lambda s: s)
|
||||||
|
|
||||||
|
root = tk.Tk()
|
||||||
|
root.withdraw()
|
||||||
|
try:
|
||||||
|
app = _DummyClusterSaveApp(root)
|
||||||
|
|
||||||
|
assert app._validate_and_save_cluster_config() is True
|
||||||
|
assert app.saved_calls == 1
|
||||||
|
|
||||||
|
# Requirement: clicking Save should immediately run a status check.
|
||||||
|
assert app.status_check_calls == 1
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
root.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
@ -12,7 +12,7 @@ from installer.state import InstallerState
|
|||||||
from installer.milestone import Milestone, ProgressCallback
|
from installer.milestone import Milestone, ProgressCallback
|
||||||
from installer.build import get_build_command, BuildMilestone
|
from installer.build import get_build_command, BuildMilestone
|
||||||
from installer.core.controller import ProleController
|
from installer.core.controller import ProleController
|
||||||
from installer.core.actions import ProleSilentInstaller
|
from installer.core.actions import ProleConsoleInstaller
|
||||||
|
|
||||||
|
|
||||||
# ===== InstallerState =====
|
# ===== InstallerState =====
|
||||||
@ -271,7 +271,7 @@ class TestProleController:
|
|||||||
assert c.project_root == tmp_path
|
assert c.project_root == tmp_path
|
||||||
assert c.verbose is False
|
assert c.verbose is False
|
||||||
assert c.state is not None
|
assert c.state is not None
|
||||||
installer = ProleSilentInstaller(c)
|
installer = ProleConsoleInstaller(c)
|
||||||
defaults = installer._default_inputs()
|
defaults = installer._default_inputs()
|
||||||
assert defaults.get("kerberos_config.user") == "administrator"
|
assert defaults.get("kerberos_config.user") == "administrator"
|
||||||
assert defaults.get("kerberos_config.password") == ""
|
assert defaults.get("kerberos_config.password") == ""
|
||||||
@ -326,7 +326,7 @@ class TestProleController:
|
|||||||
|
|
||||||
def test_silent_installer_runs_supabase_preload_before_deploy(self, tmp_path):
|
def test_silent_installer_runs_supabase_preload_before_deploy(self, tmp_path):
|
||||||
c = ProleController(tmp_path)
|
c = ProleController(tmp_path)
|
||||||
installer = ProleSilentInstaller(c)
|
installer = ProleConsoleInstaller(c)
|
||||||
|
|
||||||
with mock.patch.object(installer, "_load_inputs_from_cfg", return_value={}), \
|
with mock.patch.object(installer, "_load_inputs_from_cfg", return_value={}), \
|
||||||
mock.patch.object(installer, "_write_cfg"), \
|
mock.patch.object(installer, "_write_cfg"), \
|
||||||
@ -349,7 +349,7 @@ class TestProleController:
|
|||||||
self, tmp_path
|
self, tmp_path
|
||||||
):
|
):
|
||||||
c = ProleController(tmp_path)
|
c = ProleController(tmp_path)
|
||||||
installer = ProleSilentInstaller(c)
|
installer = ProleConsoleInstaller(c)
|
||||||
|
|
||||||
openbao_ref = "${OPENBAO:kv/prole/test/db#password}"
|
openbao_ref = "${OPENBAO:kv/prole/test/db#password}"
|
||||||
|
|
||||||
|
|||||||
7
tests/installer/test_gitea_init_script_present.py
Normal file
7
tests/installer/test_gitea_init_script_present.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from installer.core.env import PROJECT_ROOT
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_gitea_script_exists():
|
||||||
|
assert (PROJECT_ROOT / "etc" / "init_gitea.sh").is_file()
|
||||||
88
tests/installer/test_namespace_propagation.py
Normal file
88
tests/installer/test_namespace_propagation.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import tkinter as tk
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from installer.ui.screens.cfg import ConfigMixin
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyNamespaceApp(ConfigMixin):
|
||||||
|
"""Minimal surface to exercise `ConfigMixin._propagate_namespace_change`."""
|
||||||
|
|
||||||
|
def __init__(self, root: tk.Tk):
|
||||||
|
self.root = root
|
||||||
|
# Keep the test fast and deterministic.
|
||||||
|
# Use a debounce long enough that the scheduled `after()` callback will not
|
||||||
|
# run between the simulated keystrokes (which call `root.update()`).
|
||||||
|
self._namespace_debounce_ms = 100
|
||||||
|
|
||||||
|
self.db_namespace = tk.StringVar(master=root, value="prole-db")
|
||||||
|
self.service_namespace = tk.StringVar(master=root, value="knoe-system")
|
||||||
|
|
||||||
|
# Independent namespaces that must not be rewritten.
|
||||||
|
self.gitops_namespace = tk.StringVar(master=root, value="gitea")
|
||||||
|
self.argocd_namespace = tk.StringVar(master=root, value="argocd")
|
||||||
|
|
||||||
|
self._last_synced_ns = self.db_namespace.get()
|
||||||
|
|
||||||
|
# Inputs dict is used by the sweep logic; include a service namespace entry
|
||||||
|
# to ensure it is not touched.
|
||||||
|
self.inputs = {
|
||||||
|
"init_cluster.service_namespace": self.service_namespace.get(),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.save_calls = 0
|
||||||
|
self.updated_namespace = None
|
||||||
|
|
||||||
|
def _update_env_namespace(self, namespace: str):
|
||||||
|
self.updated_namespace = namespace
|
||||||
|
|
||||||
|
def _save_prole_cfg(self):
|
||||||
|
self.save_calls += 1
|
||||||
|
|
||||||
|
|
||||||
|
def _drain_tk_events(root: tk.Tk, timeout_s: float = 0.2) -> None:
|
||||||
|
end = time.monotonic() + timeout_s
|
||||||
|
while time.monotonic() < end:
|
||||||
|
root.update()
|
||||||
|
time.sleep(0.001)
|
||||||
|
|
||||||
|
|
||||||
|
def test_namespace_typing_does_not_corrupt_service_namespace():
|
||||||
|
# Some test modules replace tkinter with MagicMocks at import-time.
|
||||||
|
if isinstance(sys.modules.get("tkinter"), MagicMock):
|
||||||
|
pytest.skip("tkinter is mocked in this test run")
|
||||||
|
|
||||||
|
root = tk.Tk()
|
||||||
|
root.withdraw()
|
||||||
|
try:
|
||||||
|
app = _DummyNamespaceApp(root)
|
||||||
|
app.db_namespace.trace_add("write", app._propagate_namespace_change)
|
||||||
|
|
||||||
|
# Simulate typing a new namespace in the UI entry box.
|
||||||
|
for partial in [
|
||||||
|
"k",
|
||||||
|
"kn",
|
||||||
|
"kno",
|
||||||
|
"knoe",
|
||||||
|
"knoe-",
|
||||||
|
"knoe-d",
|
||||||
|
"knoe-db",
|
||||||
|
]:
|
||||||
|
app.db_namespace.set(partial)
|
||||||
|
root.update()
|
||||||
|
|
||||||
|
_drain_tk_events(root, timeout_s=0.3)
|
||||||
|
|
||||||
|
# Regression: SERVICE_NAMESPACE must remain unchanged.
|
||||||
|
assert app.service_namespace.get() == "knoe-system"
|
||||||
|
|
||||||
|
# Debounced propagation should result in a single save.
|
||||||
|
assert app.save_calls == 1
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
root.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
74
tests/installer/test_push_docker_image_ca_fallback.py
Normal file
74
tests/installer/test_push_docker_image_ca_fallback.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
|
||||||
|
def test_push_docker_image_prefers_tls_verified_skopeo_when_ca_provided(
|
||||||
|
tmp_path, monkeypatch
|
||||||
|
):
|
||||||
|
from installer.core import env
|
||||||
|
|
||||||
|
ca = tmp_path / "registry-ca.pem"
|
||||||
|
ca.write_text("dummy-ca")
|
||||||
|
monkeypatch.setenv("PROLE_REGISTRY_CA_CERT", str(ca))
|
||||||
|
|
||||||
|
monkeypatch.setattr(env.shutil, "which", lambda name: "/usr/bin/skopeo" if name == "skopeo" else None)
|
||||||
|
monkeypatch.setattr(env.shutil, "copyfile", lambda _src, _dst: None)
|
||||||
|
|
||||||
|
calls: list[list[str]] = []
|
||||||
|
|
||||||
|
def fake_run(cmd, capture_output=True, text=True):
|
||||||
|
calls.append(list(cmd))
|
||||||
|
if cmd[:2] == ["docker", "push"]:
|
||||||
|
return SimpleNamespace(returncode=1, stdout="", stderr="x509: unknown authority")
|
||||||
|
|
||||||
|
# Secure skopeo path should be used and succeed.
|
||||||
|
assert cmd[0] == "/usr/bin/skopeo"
|
||||||
|
assert "--dest-tls-verify=true" in cmd
|
||||||
|
assert "--dest-cert-dir" in cmd
|
||||||
|
assert "--dest-tls-verify=false" not in cmd
|
||||||
|
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||||
|
|
||||||
|
monkeypatch.setattr(env.subprocess, "run", fake_run)
|
||||||
|
|
||||||
|
ok = env._push_docker_image("myrddin.prole.org:5000/prole-db:18-118")
|
||||||
|
assert ok is True
|
||||||
|
assert len(calls) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_push_docker_image_auto_detects_repo_cert_for_myrddin(monkeypatch):
|
||||||
|
"""Regression: when no env var is provided, prefer repo-shipped certs (Ansible prole_ssl role)."""
|
||||||
|
|
||||||
|
from installer.core import env
|
||||||
|
|
||||||
|
monkeypatch.delenv("PROLE_REGISTRY_CA_CERT", raising=False)
|
||||||
|
monkeypatch.delenv("REGISTRY_CA_CERT", raising=False)
|
||||||
|
monkeypatch.delenv("PROLE_REGISTRY_CERT_DIR", raising=False)
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
env.shutil,
|
||||||
|
"which",
|
||||||
|
lambda name: "/usr/bin/skopeo" if name == "skopeo" else None,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(env.shutil, "copyfile", lambda _src, _dst: None)
|
||||||
|
|
||||||
|
calls: list[list[str]] = []
|
||||||
|
|
||||||
|
def fake_run(cmd, capture_output=True, text=True):
|
||||||
|
calls.append(list(cmd))
|
||||||
|
if cmd[:2] == ["docker", "push"]:
|
||||||
|
return SimpleNamespace(
|
||||||
|
returncode=1, stdout="", stderr="x509: unknown authority"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert cmd[0] == "/usr/bin/skopeo"
|
||||||
|
assert "--dest-tls-verify=true" in cmd
|
||||||
|
assert "--dest-cert-dir" in cmd
|
||||||
|
assert "--dest-tls-verify=false" not in cmd
|
||||||
|
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||||
|
|
||||||
|
monkeypatch.setattr(env.subprocess, "run", fake_run)
|
||||||
|
|
||||||
|
ok = env._push_docker_image("myrddin.prole.org:5000/prole-db:18-118")
|
||||||
|
assert ok is True
|
||||||
|
assert len(calls) == 2
|
||||||
71
tests/installer/test_registry_k3s_selection.py
Normal file
71
tests/installer/test_registry_k3s_selection.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
from installer.ui.screens.docker import DockerScreenMixin
|
||||||
|
import installer.ui.screens.docker as docker_mod
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyDockerScreen(DockerScreenMixin):
|
||||||
|
def __init__(self, tmp_path: Path):
|
||||||
|
self._tmp_path = tmp_path
|
||||||
|
self.prole_cfg_data = {
|
||||||
|
# Global deployment mode may remain k3d, while the UI selection is Service.
|
||||||
|
"Global": {"DEPLOYMENT_MODE": "k3d"},
|
||||||
|
"Docker Build": {},
|
||||||
|
}
|
||||||
|
self.local_registry_url = None
|
||||||
|
self.local_registry_internal = None
|
||||||
|
|
||||||
|
def _cluster_env_key(self, env_label=None) -> str:
|
||||||
|
return "service"
|
||||||
|
|
||||||
|
def _deployment_mode(self) -> str:
|
||||||
|
return "k3d"
|
||||||
|
|
||||||
|
def _k3s_registry_hostport(self) -> str:
|
||||||
|
return "myrddin.prole.org:5000"
|
||||||
|
|
||||||
|
def _registry_namespace(self) -> str:
|
||||||
|
return "knoe-system"
|
||||||
|
|
||||||
|
def check_docker_running(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def safe_after(self, fn):
|
||||||
|
# Execute inline for tests
|
||||||
|
try:
|
||||||
|
fn()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _save_prole_cfg(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _registry_log_path(self) -> Path:
|
||||||
|
return self._tmp_path / "local_registry.log"
|
||||||
|
|
||||||
|
|
||||||
|
def test_ensure_local_registry_available_prefers_selected_env_over_global_mode(monkeypatch, tmp_path):
|
||||||
|
"""Regression: Service (k3s) selection must not trigger localhost/k3d registry checks."""
|
||||||
|
|
||||||
|
def _unexpected_ping(*_args, **_kwargs):
|
||||||
|
raise AssertionError("_http_ping_registry should not be called for k3s branch")
|
||||||
|
|
||||||
|
def _unexpected_run(*args, **kwargs):
|
||||||
|
raise AssertionError(f"subprocess.run should not be used for k3s branch: {args}")
|
||||||
|
|
||||||
|
monkeypatch.setattr(docker_mod, "_http_ping_registry", _unexpected_ping)
|
||||||
|
monkeypatch.setattr(docker_mod.subprocess, "run", _unexpected_run)
|
||||||
|
|
||||||
|
screen = _DummyDockerScreen(tmp_path)
|
||||||
|
info = screen.ensure_local_registry_available()
|
||||||
|
assert info == (
|
||||||
|
"myrddin.prole.org:5000",
|
||||||
|
"registry.knoe-system.svc.cluster.local:5000",
|
||||||
|
)
|
||||||
30
tests/installer/test_registry_rescan_after_init.py
Normal file
30
tests/installer/test_registry_rescan_after_init.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
|
||||||
|
def test_db_rescan_registry_status_after_init_schedules_multiple_checks():
|
||||||
|
from installer.ui.screens.database import DatabaseScreenMixin
|
||||||
|
|
||||||
|
class _Dummy(DatabaseScreenMixin):
|
||||||
|
def __init__(self):
|
||||||
|
self._scheduled: list[int] = []
|
||||||
|
self._ensure_called = 0
|
||||||
|
self._db_registry_status_label = 1
|
||||||
|
self.bg_canvas = MagicMock()
|
||||||
|
|
||||||
|
def safe_after(self, fn, delay: int = 0):
|
||||||
|
self._scheduled.append(delay)
|
||||||
|
if delay == 0:
|
||||||
|
return fn()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _ensure_db_build_registry_async(self):
|
||||||
|
self._ensure_called += 1
|
||||||
|
|
||||||
|
d = _Dummy()
|
||||||
|
d._db_rescan_registry_status_after_init()
|
||||||
|
|
||||||
|
# One immediate label update (delay 0), then a sequence of scheduled checks.
|
||||||
|
assert d._scheduled == [0, 0, 1000, 2000, 4000, 8000, 15000]
|
||||||
|
assert d._ensure_called == 1
|
||||||
141
tests/test_cluster_screen_observe_only.py
Normal file
141
tests/test_cluster_screen_observe_only.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
from installer.ui.screens.cluster import ClusterScreenMixin
|
||||||
|
|
||||||
|
|
||||||
|
class _Var:
|
||||||
|
def __init__(self, value: str = ""):
|
||||||
|
self._value = value
|
||||||
|
|
||||||
|
def get(self) -> str:
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
def set(self, value: str) -> None:
|
||||||
|
self._value = value
|
||||||
|
|
||||||
|
|
||||||
|
class _Root:
|
||||||
|
def after(self, _delay_ms: int, fn):
|
||||||
|
fn()
|
||||||
|
|
||||||
|
|
||||||
|
class _Canvas:
|
||||||
|
def itemconfig(self, *_args, **_kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class _Button:
|
||||||
|
def configure(self, **_kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def test_cluster_status_refresh_is_observe_only(monkeypatch):
|
||||||
|
"""Regression: simply navigating/refreshing the Cluster Environment screen must not mutate the cluster."""
|
||||||
|
|
||||||
|
class _ImmediateThread:
|
||||||
|
def __init__(self, *, target=None, daemon=None):
|
||||||
|
self._target = target
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
# Execute synchronously so exceptions surface to the test.
|
||||||
|
if self._target:
|
||||||
|
self._target()
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"installer.ui.screens.cluster.threading.Thread", _ImmediateThread, raising=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Dummy(ClusterScreenMixin):
|
||||||
|
def __init__(self):
|
||||||
|
self.root = _Root()
|
||||||
|
self.bg_canvas = _Canvas()
|
||||||
|
self.k3d_status_label = 1
|
||||||
|
self.common_services_status_label = 2
|
||||||
|
self._init_cluster_button = _Button()
|
||||||
|
self._kong_last_repair_at = 0.0
|
||||||
|
self._kong_repair_cooldown_s = 0.0
|
||||||
|
self._repair_inflight = False
|
||||||
|
self._repair_last_run_at = 0.0
|
||||||
|
self._repair_cooldown_s = 0.0
|
||||||
|
|
||||||
|
def _set_cluster_env_message(self, *_args, **_kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _cluster_status_snapshot(self) -> dict:
|
||||||
|
return {
|
||||||
|
"env": "service",
|
||||||
|
"cluster_ok": True,
|
||||||
|
"cluster_msg": "ok",
|
||||||
|
"cluster_fill": "#34c759",
|
||||||
|
"common_ok": True,
|
||||||
|
"common_msg": "ok",
|
||||||
|
"common_fill": "#34c759",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _dashboard_kong_status(self, _env=None):
|
||||||
|
# Unhealthy, but observe-only behavior must not attempt repairs.
|
||||||
|
return {"ok": False, "reset_pods": ["pod-1"]}
|
||||||
|
|
||||||
|
def _authority_context_missing(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _maybe_run_repair_pipeline(self, *_args, **_kwargs):
|
||||||
|
raise AssertionError("repair pipeline must not run during status refresh")
|
||||||
|
|
||||||
|
Dummy().check_cluster_status_async()
|
||||||
|
|
||||||
|
|
||||||
|
def test_service_namespace_infers_registry_namespace(monkeypatch):
|
||||||
|
"""If cluster is reachable and `registry:2` exists, infer that namespace instead of defaulting to `default`."""
|
||||||
|
|
||||||
|
class Dummy(ClusterScreenMixin):
|
||||||
|
def __init__(self):
|
||||||
|
self.service_namespace = _Var("")
|
||||||
|
self.cluster_env = _Var("prod")
|
||||||
|
self.prole_cfg_data = {"Global": {}}
|
||||||
|
|
||||||
|
def _ensure_k3s_kubeconfig_merged(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _deployment_mode(self) -> str:
|
||||||
|
return "k8s"
|
||||||
|
|
||||||
|
def _kubectl_base_cmd(self, _mode=None):
|
||||||
|
return ["kubectl"]
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"metadata": {"namespace": "knoe-system"},
|
||||||
|
"spec": {
|
||||||
|
"template": {
|
||||||
|
"spec": {"containers": [{"image": "registry:2"}]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def fake_run(cmd, capture_output, text, env, timeout):
|
||||||
|
assert cmd[:3] == ["kubectl", "get", "deploy"]
|
||||||
|
|
||||||
|
class _Res:
|
||||||
|
returncode = 0
|
||||||
|
stdout = json.dumps(payload)
|
||||||
|
stderr = ""
|
||||||
|
|
||||||
|
return _Res()
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"installer.ui.screens.cluster.subprocess.run", fake_run, raising=True
|
||||||
|
)
|
||||||
|
# Avoid reliance on an actual kubeconfig in test environment.
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"installer.ui.screens.cluster._find_kubeconfig_file", lambda *_a, **_k: "", raising=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Dummy()._get_service_namespace() == "knoe-system"
|
||||||
19
tests/test_init_kerberos_ad_forwarder_readiness.py
Normal file
19
tests/test_init_kerberos_ad_forwarder_readiness.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_kerberos_waits_for_ad_forwarder_ready_and_endpoints():
|
||||||
|
"""Regression: Kerberos test must not run against an AD forwarder Service with no endpoints.
|
||||||
|
|
||||||
|
Historically `init_kerberos.sh` would `kubectl apply` the AD forwarder Deployment/Service and then
|
||||||
|
proceed immediately (or ignore rollout failures), which could yield `kinit: Cannot contact any KDC`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
text = Path("etc/init_kerberos.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "wait_for_ad_forwarder_ready" in text
|
||||||
|
assert "wait_for_ad_forwarder_ready \"$KRB5_AD_NAMESPACE\" \"$KRB5_AD_PROXY_NAME\" \"$KRB5_AD_SERVICE_NAME\"" in text
|
||||||
|
|
||||||
|
# Ensure we don't silently ignore readiness.
|
||||||
|
assert "rollout status deploy/${KRB5_AD_PROXY_NAME} --timeout=120s || true" not in text
|
||||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from installer.core.actions import ProleSilentInstaller
|
from installer.core.actions import ProleConsoleInstaller
|
||||||
from installer.core.controller import ProleController
|
from installer.core.controller import ProleController
|
||||||
|
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ def test_silent_installer_service_namespace_defaults_to_knoe_system_for_k3s(tmp_
|
|||||||
monkeypatch.delenv("SERVICE_NAMESPACE", raising=False)
|
monkeypatch.delenv("SERVICE_NAMESPACE", raising=False)
|
||||||
|
|
||||||
c = ProleController(tmp_path)
|
c = ProleController(tmp_path)
|
||||||
installer = ProleSilentInstaller(c)
|
installer = ProleConsoleInstaller(c)
|
||||||
installer.inputs["init_cluster.cluster_env"] = "service"
|
installer.inputs["init_cluster.cluster_env"] = "service"
|
||||||
installer.prole_cfg_data = {"Global": {}}
|
installer.prole_cfg_data = {"Global": {}}
|
||||||
|
|
||||||
|
|||||||
@ -5,13 +5,13 @@ from types import SimpleNamespace
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import installer.core.actions as actions
|
import installer.core.actions as actions
|
||||||
from installer.core.actions import ProleSilentInstaller
|
from installer.core.actions import ProleConsoleInstaller
|
||||||
from installer.core.controller import ProleController
|
from installer.core.controller import ProleController
|
||||||
|
|
||||||
|
|
||||||
def _mk_installer(tmp_path) -> ProleSilentInstaller:
|
def _mk_installer(tmp_path) -> ProleConsoleInstaller:
|
||||||
c = ProleController(tmp_path)
|
c = ProleController(tmp_path)
|
||||||
return ProleSilentInstaller(c)
|
return ProleConsoleInstaller(c)
|
||||||
|
|
||||||
|
|
||||||
def test_repair_flow_classifies_blockers_attempts_reclaim_and_avoids_blind_health_wait(
|
def test_repair_flow_classifies_blockers_attempts_reclaim_and_avoids_blind_health_wait(
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import os
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
import installer.core.actions as actions
|
import installer.core.actions as actions
|
||||||
from installer.core.actions import ProleSilentInstaller
|
from installer.core.actions import ProleConsoleInstaller
|
||||||
from installer.core.controller import ProleController
|
from installer.core.controller import ProleController
|
||||||
|
|
||||||
|
|
||||||
@ -29,9 +29,9 @@ def _preserve_namespace_env():
|
|||||||
os.environ[k] = v
|
os.environ[k] = v
|
||||||
|
|
||||||
|
|
||||||
def _mk_installer(tmp_path) -> ProleSilentInstaller:
|
def _mk_installer(tmp_path) -> ProleConsoleInstaller:
|
||||||
c = ProleController(tmp_path)
|
c = ProleController(tmp_path)
|
||||||
return ProleSilentInstaller(c)
|
return ProleConsoleInstaller(c)
|
||||||
|
|
||||||
|
|
||||||
def test_match_stale_released_pv_happy_path(tmp_path):
|
def test_match_stale_released_pv_happy_path(tmp_path):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user