Merge branch 'main' of github.com:dredx/prole

This commit is contained in:
chrisfu 2026-01-18 20:03:18 -08:00
commit 9b50fb963d
87 changed files with 7 additions and 13133 deletions

1
.gitignore vendored
View File

@ -44,3 +44,4 @@ htmlcov/
/workstation/Prole/Saved/
/workstation/Prole/DerivedDataCache/
/workstation/Prole/Build/
/ssh-keys/

View File

@ -1,29 +0,0 @@
#!/usr/bin/env bash
# Prole environment configuration
# This file is generated by the installer. Source it in new shells, or execute as a wrapper:
# "$PROLE_HOME/env.sh" <command> [args…]
# shellcheck shell=bash
export PROLE_HOME="<MagicMock name='mock.Entry().get().strip()' id='4411745888'>"
export PROLE_CONF="<MagicMock name='mock.Entry().get().strip()' id='4411745888'>"
export PROLE_DATA="<MagicMock name='mock.Entry().get().strip()' id='4411745888'>"
export PROLE_LOGS="<MagicMock name='mock.Entry().get().strip()' id='4411745888'>"
export PROLE_SERVICE="<MagicMock name='mock.Entry().get().strip()' id='4411745888'>"
# Ensure PATH works for GUI-launched shells (Docker, Ollama, etc.)
_prole_add_path() { case ":${PATH}:" in *":$1:"*) ;; *) PATH="$1:${PATH:-}" ;; esac; }
_prole_add_path "$PROLE_HOME/bin"
_prole_add_path "/opt/homebrew/bin"
_prole_add_path "/usr/local/bin"
_prole_add_path "/usr/bin"
_prole_add_path "/bin"
_prole_add_path "/usr/sbin"
_prole_add_path "/sbin"
export PATH
# Add custom paths below if needed (examples):
# _prole_add_path "/Applications/Ollama.app/Contents/MacOS"
# If executed with arguments, run them under this environment
if [ "$#" -gt 0 ]; then
exec "$@"
fi

View File

@ -1,15 +0,0 @@
myrddin.prole.org 10.0.0.3
raspberry.prole.org 10.0.0.4
pi.prole.org 10.0.0.5
synology.prole.org 10.0.0.203
morgoth.prole.org 10.0.0.204
zinfandel.prole.org 10.0.0.205
aventage.prole.org 10.0.0.206
retropie.prole.org 10.0.0.207
fairyland.prole.org 10.0.0.208
k8s.prole.org zinfandel.prole.org
mc.prole.org 73.15.20.166
morana.prole.org 10.0.0.66
ollama.prole.org 73.15.20.166
svc.prole.org 73.15.20.166
www.prole.org ghs.googlehosted.com

View File

@ -1,326 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# init_cloudnative_pg.sh
# Purpose:
# - Distribute administrator ed25519 key pair to CloudNativePG as a Kubernetes Secret for cert auth
# - Configure Kerberos (GSSAPI) using external Kerberos KDC
# - Patch CNPG cluster to enable TLS and GSSAPI where possible
#
# Usage:
# ./init_cloudnative_pg.sh start|stop|status|restart
# ./init_cloudnative_pg.sh initialize # create/update k8s secrets/configs and patch CNPG
# ./init_cloudnative_pg.sh update|reload # re-apply/patch
#
# Requirements:
# - init_openbao.sh has been run (OpenBao running in k8s)
# - $PROLE_HOME/env.sh or $HOME/.prole/env.sh defining PROLE_SERVICE
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Load env without leaking our positional args to the env script (some env.sh may `exec "$@"`).
__PROLE_SAVED_ARGS=("$@")
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$HOME/.prole/env.sh"
else
echo "ERROR: Missing env. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
exit 1
fi
set -- "${__PROLE_SAVED_ARGS[@]}"
unset __PROLE_SAVED_ARGS
if [[ -z "${PROLE_SERVICE:-}" ]]; then
echo "ERROR: PROLE_SERVICE is not defined in env." >&2
exit 1
fi
ACTION=${1:-}
CNPG_CLUSTER_NAME=${2:-${CNPG_CLUSTER_NAME:-prole-db}}
NAMESPACE=${NAMESPACE:-prole}
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
REALM=${REALM:-PROLE.ORG}
DOMAIN=${DOMAIN:-prole.org}
KRB5_KDC=${KRB5_KDC:-}
KRB5_ADMIN=${KRB5_ADMIN:-}
CNPG_MANIFEST="$SCRIPT_DIR/../k8s/prole/prole-db.yaml"
SECRETS_DIR="$PROLE_SERVICE/secrets"
ADMIN_PRIV="$SECRETS_DIR/admin_ed25519.key"
ADMIN_PUB="$SECRETS_DIR/admin_ed25519.pub"
OPENBAO_TOKEN_FILE="$SECRETS_DIR/openbao-root-token"
ensure_tools() {
for t in kubectl curl openssl base64 jq; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
# Resolve OpenBao URL: prefer explicit env, then localhost port-forward, then cluster DNS
bao_service_url() {
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
echo "$PROLE_OPENBAO_URL"
return 0
fi
# Prefer standard local port-forward managed by etc/init_port_forwards.sh
if curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:18200"
return 0
fi
echo "http://$OPENBAO_NAME.$NAMESPACE.svc.cluster.local:8200"
}
fetch_admin_keys_and_db_pass_from_bao_or_local() {
local token url
if [[ -f "$OPENBAO_TOKEN_FILE" ]]; then
token=$(cat "$OPENBAO_TOKEN_FILE")
else
token=""
fi
url=$(bao_service_url)
if [[ -n "$token" ]]; then
echo "Attempting to read admin key pair from OpenBao kv/prole/admin ..."
if curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -e '.data.data' >/dev/null 2>&1; then
local priv_b64 pub_b64
priv_b64=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -r '.data.data.admin_private_key_b64')
pub_b64=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -r '.data.data.admin_public_key_b64')
printf "%s" "$priv_b64" | base64 -d >"$ADMIN_PRIV"
printf "%s" "$pub_b64" | base64 -d >"$ADMIN_PUB"
chmod 0600 "$ADMIN_PRIV"
fi
echo "Attempting to read database password from OpenBao kv/prole/db ..."
if curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/db" | jq -e '.data.data' >/dev/null 2>&1; then
local db_pass
db_pass=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/db" | jq -r '.data.data.password')
if [[ -n "$db_pass" ]]; then
echo "Updating database user secret 'prole-db-user' from OpenBao ..."
kubectl create secret generic prole-db-user -n "$NAMESPACE" \
--from-literal=username=prole \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -f -
echo "Updating database superuser secret 'prole-db-superuser' from OpenBao ..."
kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \
--from-literal=username=postgres \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -f -
fi
fi
fi
if [[ -f "$ADMIN_PRIV" && -f "$ADMIN_PUB" ]]; then
echo "Using local admin key pair at $SECRETS_DIR"
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() {
echo "Creating/updating Secret cnpg-admin-key ..."
kubectl create secret generic cnpg-admin-key -n "$NAMESPACE" \
--from-file=admin.key="$ADMIN_PRIV" \
--from-file=admin.pub="$ADMIN_PUB" \
--dry-run=client -o yaml | kubectl apply -f -
}
ensure_krb5_conf_configmap() {
echo "Creating/updating ConfigMap prole-krb5-conf ..."
local kdc_val admin_val
if [[ -n "$KRB5_KDC" ]]; then
kdc_val="$KRB5_KDC"
admin_val=${KRB5_ADMIN:-$(echo "$KRB5_KDC" | cut -d, -f1)}
else
kdc_val="kdc.$DOMAIN"
admin_val="kdc.$DOMAIN"
fi
local TMP
TMP=$(mktemp)
cat >"$TMP" <<EOF
[libdefaults]
default_realm = $REALM
dns_lookup_realm = true
dns_lookup_kdc = true
[realms]
$REALM = {
kdc = $kdc_val
admin_server = $admin_val
}
[domain_realm]
.$DOMAIN = $REALM
$DOMAIN = $REALM
EOF
kubectl -n "$NAMESPACE" create configmap prole-krb5-conf --from-file=krb5.conf="$TMP" --dry-run=client -o yaml | kubectl apply -f -
rm -f "$TMP"
}
generate_tls_if_missing() {
local ca_secret_name="${CNPG_CLUSTER_NAME}-ca"
if kubectl -n "$NAMESPACE" get secret "$ca_secret_name" >/dev/null 2>&1; then
echo "CA secret $ca_secret_name already exists; skipping generation."
return 0
fi
echo "Generating self-signed CA (RSA 4096) for CNPG ..."
local TMPD
TMPD=$(mktemp -d)
openssl genrsa -out "$TMPD/ca.key" 4096
openssl req -x509 -new -key "$TMPD/ca.key" -out "$TMPD/ca.crt" -days 3650 -subj "/CN=Prole CNPG CA"
kubectl -n "$NAMESPACE" create secret generic "$ca_secret_name" \
--from-file=ca.crt="$TMPD/ca.crt" \
--from-file=ca.key="$TMPD/ca.key" \
--dry-run=client -o yaml | kubectl apply -f -
rm -rf "$TMPD"
}
patch_cnpg_cluster_for_auth() {
echo "Patching CNPG Cluster $CNPG_CLUSTER_NAME to enable GSSAPI and fix config (best-effort) ..."
# Best-effort patch; schema may vary with CNPG version.
# We remove krb_srvname as it is unrecognized in some PG 17 builds.
# We revert certificates to default to avoid operator TLS issues.
kubectl -n "$NAMESPACE" patch cluster "$CNPG_CLUSTER_NAME" --type merge -p "{
\"spec\": {
\"postgresql\": {
\"parameters\": {\"krb_srvname\": null},
\"pg_hba\": [
\"local all postgres trust\",
\"host all postgres all scram-sha-256\",
\"host all all all gss include_realm=1 krb_realm=$REALM\",
\"host all all all scram-sha-256\"
]
},
\"certificates\": {
\"serverCASecret\": null,
\"clientCASecret\": null
}
}
}" >/dev/null || echo "Note: patch may need adjustment for your CNPG version."
}
initialize() {
ensure_tools
# Ensure port-forward is running for OpenBao (dependency)
echo "Ensuring port-forward for OpenBao is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart openbao
fetch_admin_keys_and_db_pass_from_bao_or_local
apply_cnpg_admin_secret
ensure_krb5_conf_configmap
# generate_tls_if_missing
patch_cnpg_cluster_for_auth
# Ensure port-forward is running for Postgres (local access)
echo "Ensuring port-forward for Postgres is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart postgres
echo "Initialization complete for CNPG + Kerberos + cert artifacts."
}
update_reload() {
initialize
}
case "$ACTION" in
recreate)
ensure_tools
"$0" delete "$CNPG_CLUSTER_NAME"
"$0" create "$CNPG_CLUSTER_NAME"
;;
create)
ensure_tools
echo "Installing CloudNative-PG operator ..."
kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.27/releases/cnpg-1.27.0.yaml
echo "Creating CloudNative-PG cluster and resources for '$CNPG_CLUSTER_NAME' ..."
kubectl apply -k "$SCRIPT_DIR/../k8s/prole"
# Ensure prole-index-html exists for prole deployment readiness probe
if ! kubectl get configmap prole-index-html -n prole >/dev/null 2>&1; then
echo "Creating prole-index-html configmap..."
printf "<html><body><h1>Prole</h1></body></html>" > /tmp/index.html
kubectl create configmap prole-index-html --from-file=/tmp/index.html -n prole
rm /tmp/index.html
fi
# Ensure prole-nginx-tls exists (self-signed for dev)
if ! kubectl get secret prole-nginx-tls -n prole >/dev/null 2>&1; then
echo "Generating self-signed prole-nginx-tls for development..."
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /tmp/nginx-tls.key -out /tmp/nginx-tls.crt \
-subj "/CN=prole.org" >/dev/null 2>&1
kubectl create secret tls prole-nginx-tls --key /tmp/nginx-tls.key --cert /tmp/nginx-tls.crt -n prole
rm /tmp/nginx-tls.key /tmp/nginx-tls.crt
fi
echo "Initializing and patching cluster ..."
initialize
;;
delete)
ensure_tools
echo "Deleting all resources for '$CNPG_CLUSTER_NAME' ..."
kubectl delete -k "$SCRIPT_DIR/../k8s/prole" --ignore-not-found
;;
start)
ensure_tools
if [[ ! -f "$CNPG_MANIFEST" ]]; then
echo "ERROR: CNPG manifest not found at $CNPG_MANIFEST" >&2
exit 1
fi
echo "Starting CloudNative-PG cluster from $CNPG_MANIFEST in namespace $NAMESPACE..."
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST"
;;
stop)
ensure_tools
if [[ ! -f "$CNPG_MANIFEST" ]]; then
echo "ERROR: CNPG manifest not found at $CNPG_MANIFEST" >&2
exit 1
fi
echo "Stopping CloudNative-PG cluster using $CNPG_MANIFEST ..."
kubectl delete -f "$CNPG_MANIFEST" --ignore-not-found
;;
status)
ensure_tools
echo "--- CloudNative-PG Cluster Status ($CNPG_CLUSTER_NAME) ---"
if kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" >/dev/null 2>&1; then
kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME"
echo ""
echo "CNPG Plugin Status:"
if kubectl cnpg version >/dev/null 2>&1; then
kubectl cnpg status "$CNPG_CLUSTER_NAME" -n "$NAMESPACE"
else
echo "Note: 'kubectl cnpg' plugin not found; skipping detailed status."
fi
else
echo "Cluster '$CNPG_CLUSTER_NAME' not found in namespace '$NAMESPACE'."
fi
;;
restart)
ensure_tools
"$0" stop
"$0" start
;;
initialize)
initialize
;;
update|reload)
update_reload
;;
*)
echo "Usage: $0 {create|delete|recreate|start|stop|status|restart|initialize|update|reload} [dbname]" >&2
exit 2
;;
esac

View File

@ -1,203 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# etc/init_k8s.sh
# Purpose: Manage k3d/k8s environment
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Preserve positional args while sourcing env
__PROLE_SAVED_ARGS=("$@")
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$HOME/.prole/env.sh"
fi
set -- "${__PROLE_SAVED_ARGS[@]}"
unset __PROLE_SAVED_ARGS
# Default values
VERBOSE=false
ENVIRONMENT="prod"
HOST=""
CLUSTER_NAME_DEFAULT="prole-dev-cluster"
CLUSTER_PORT_DEFAULT="6443"
usage() {
cat <<EOF
Usage: $0 [initialize|start|stop|restart|status] [options]
Actions:
initialize Create a k3d cluster (dev) or fetch kubeconfig (service/prod)
start Start the k3d cluster
stop Stop the k3d cluster
restart Restart the k3d cluster
status Show status of Docker and k3d cluster
Options:
-v, --verbose Enable verbose output
-e, --environment dev|service|prod (default: $ENVIRONMENT)
-h, --host Remote host for service/prod environments
-n, --name k3d cluster name
EOF
exit 1
}
log() {
echo "[INFO] $*"
}
debug() {
if [[ "$VERBOSE" == "true" ]]; then
echo "[DEBUG] $*"
fi
}
# Parse options
POSITIONAL_ARGS=()
CLUSTER_NAME=""
while [[ $# -gt 0 ]]; do
case $1 in
-v|--verbose)
VERBOSE=true
shift
;;
-e|--environment)
ENVIRONMENT="$2"
shift 2
;;
-h|--host)
HOST="$2"
shift 2
;;
-n|--name)
CLUSTER_NAME="$2"
shift 2
;;
*)
POSITIONAL_ARGS+=("$1")
shift
;;
esac
done
set -- "${POSITIONAL_ARGS[@]}"
ACTION=${1:-}
if [[ -z "$ACTION" ]]; then
usage
fi
ensure_tools() {
for t in k3d docker; do
command -v "$t" >/dev/null || { echo "ERROR: Missing required tool: $t" >&2; exit 1; }
done
}
ensure_docker() {
if ! docker info >/dev/null 2>&1; then
echo "ERROR: Docker is not running." >&2
exit 1
fi
}
initialize() {
ensure_tools
ensure_docker
if [[ "$ENVIRONMENT" == "dev" ]]; then
if [[ -z "$CLUSTER_NAME" ]]; then
if [[ -t 0 ]]; then
read -p "Enter k3d cluster name [$CLUSTER_NAME_DEFAULT]: " CLUSTER_NAME
CLUSTER_NAME=${CLUSTER_NAME:-$CLUSTER_NAME_DEFAULT}
else
CLUSTER_NAME=$CLUSTER_NAME_DEFAULT
fi
fi
if k3d cluster list "$CLUSTER_NAME" >/dev/null 2>&1; then
log "Cluster '$CLUSTER_NAME' already exists."
else
log "Creating k3d cluster '$CLUSTER_NAME'..."
k3d cluster create "$CLUSTER_NAME" -p "0.0.0.0:$CLUSTER_PORT_DEFAULT:6443@server:0"
fi
else
if [[ -n "$HOST" ]]; then
log "Environment is $ENVIRONMENT. Host is $HOST."
log "TODO: Fetch kubeconfig from $HOST (placeholder)."
else
log "Environment is $ENVIRONMENT. Use -h|--host to specify the remote cluster host."
fi
fi
}
status() {
ensure_tools
log "Checking Docker status..."
if docker info >/dev/null 2>&1; then
echo "Docker is running."
else
echo "Docker is NOT running."
fi
log "Listing k3d clusters..."
k3d cluster list
}
get_cluster_name() {
# If a name was provided or exists in env, use it.
# Otherwise, if there is only one cluster, use it.
# Finally, use default.
if [[ -n "${CLUSTER_NAME:-}" ]]; then
echo "$CLUSTER_NAME"
return
fi
local clusters
clusters=$(k3d cluster list --no-headers | awk '{print $1}')
local count
count=$(echo "$clusters" | grep -c . || true)
if [[ "$count" -eq 1 ]]; then
echo "$clusters"
else
echo "$CLUSTER_NAME_DEFAULT"
fi
}
case "$ACTION" in
initialize)
initialize
;;
status)
status
;;
start)
ensure_tools
CLUSTER_NAME=$(get_cluster_name)
log "Starting k3d cluster '$CLUSTER_NAME'..."
k3d cluster start "$CLUSTER_NAME"
;;
stop)
ensure_tools
CLUSTER_NAME=$(get_cluster_name)
log "Stopping k3d cluster '$CLUSTER_NAME'..."
k3d cluster stop "$CLUSTER_NAME"
;;
restart)
ensure_tools
CLUSTER_NAME=$(get_cluster_name)
log "Restarting k3d cluster '$CLUSTER_NAME'..."
k3d cluster stop "$CLUSTER_NAME"
k3d cluster start "$CLUSTER_NAME"
;;
*)
usage
;;
esac

View File

@ -1,490 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# init_openbao.sh
# Purpose:
# - Deploy OpenBao to Kubernetes (dev mode) and store admin ed25519 key pair
# Generate and apply a Kerberos krb5.conf ConfigMap for an external realm
# - Local Docker helpers for OpenBao (optional)
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Preserve positional args while sourcing env
__PROLE_SAVED_ARGS=("$@")
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$HOME/.prole/env.sh"
fi
set -- "${__PROLE_SAVED_ARGS[@]}"
unset __PROLE_SAVED_ARGS
if [[ -z "${PROLE_SERVICE:-}" ]]; then
echo "ERROR: PROLE_SERVICE is not defined in env. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
exit 1
fi
ACTION=${1:-}
# Defaults
NAMESPACE=${NAMESPACE:-prole}
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
OPENBAO_MANIFEST_DIR="$SCRIPT_DIR/../k8s/openbao"
# Kerberos realm defaults
KRB5_REALM=${KRB5_REALM:-PROLE.ORG}
DOMAIN=${DOMAIN:-$(echo "$KRB5_REALM" | tr 'A-Z' 'a-z')}
KRB5_KDC=${KRB5_KDC:-kdc.$DOMAIN}
KRB5_ADMIN=${KRB5_ADMIN:-}
# Secrets and token locations
SECRETS_DIR="$PROLE_SERVICE/secrets"
mkdir -p "$SECRETS_DIR"
admin_key_priv="$SECRETS_DIR/admin_ed25519.key"
admin_key_pub="$SECRETS_DIR/admin_ed25519.pub"
root_token_file="$SECRETS_DIR/openbao-root-token"
ensure_tools() {
for t in kubectl curl openssl base64; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
ensure_docker() {
command -v docker >/dev/null || { echo "Missing required tool: docker" >&2; exit 1; }
}
ensure_admin_keypair() {
# Ensure admin ed25519 keypair exists and a root token is available
mkdir -p "$SECRETS_DIR"
if [[ ! -f "$admin_key_priv" || ! -f "$admin_key_pub" ]]; then
echo "Generating admin ed25519 keypair in $SECRETS_DIR ..."
openssl genpkey -algorithm ED25519 -out "$admin_key_priv"
openssl pkey -in "$admin_key_priv" -pubout -out "$admin_key_pub"
chmod 0600 "$admin_key_priv"
fi
# Ensure a root token exists for OpenBao dev server interactions
if [[ ! -f "$root_token_file" || ! -s "$root_token_file" ]]; then
openssl rand -hex 24 >"$root_token_file"
chmod 0600 "$root_token_file"
fi
}
generate_openbao_manifests() {
mkdir -p "$OPENBAO_MANIFEST_DIR"
# Only create a simple deployment if none exists; otherwise respect current file
local deploy="$OPENBAO_MANIFEST_DIR/deployment.yaml"
if [[ ! -f "$deploy" ]]; then
cat >"$deploy" <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: $OPENBAO_NAME
namespace: $NAMESPACE
spec:
replicas: 1
selector:
matchLabels:
app: $OPENBAO_NAME
template:
metadata:
labels:
app: $OPENBAO_NAME
spec:
containers:
- name: $OPENBAO_NAME
image: $OPENBAO_IMAGE
args: ["server","-dev","-dev-listen-address=0.0.0.0:8200"]
ports:
- containerPort: 8200
---
apiVersion: v1
kind: Service
metadata:
name: $OPENBAO_NAME
namespace: $NAMESPACE
spec:
selector:
app: $OPENBAO_NAME
ports:
- name: http
port: 8200
targetPort: 8200
EOF
fi
}
generate_kerberos_configmap() {
mkdir -p "$OPENBAO_MANIFEST_DIR"
local domain_lower
domain_lower=$(echo "$DOMAIN" | tr 'A-Z' 'a-z')
# Split KRB5_KDC on comma into bash array
local IFS=','
read -r -a __kdcs <<< "$KRB5_KDC"
local kdc_lines=""
local kdc
for kdc in "${__kdcs[@]}"; do
kdc_lines+=$' kdc = '"$kdc"$'\n'
done
local admin_server admin_line
admin_server=${KRB5_ADMIN:-"${__kdcs[0]:-}"}
admin_line=""
if [[ -n "$admin_server" ]]; then
# 8 spaces to remain within the YAML literal block indentation
admin_line=$' admin_server = '"$admin_server"$'\n'
fi
cat >"$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml" <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: prole-krb5-conf
namespace: $NAMESPACE
data:
krb5.conf: |
[libdefaults]
default_realm = $KRB5_REALM
dns_lookup_realm = true
dns_lookup_kdc = true
[realms]
$KRB5_REALM = {
${kdc_lines}${admin_line} }
[domain_realm]
.$domain_lower = $KRB5_REALM
$domain_lower = $KRB5_REALM
EOF
}
apply_k8s() {
echo "Applying OpenBao manifest to namespace '$NAMESPACE' ..."
if [[ -f "$SCRIPT_DIR/../k8s/prole/openbao-statefulset.yaml" ]]; then
kubectl apply -n "$NAMESPACE" -f "$SCRIPT_DIR/../k8s/prole/openbao-statefulset.yaml"
kubectl apply -n "$NAMESPACE" -f "$SCRIPT_DIR/../k8s/prole/openbao-service.yaml"
else
kubectl apply -n "$NAMESPACE" -f "$OPENBAO_MANIFEST_DIR/deployment.yaml"
fi
echo "Applying Kerberos ConfigMap (external realm) to namespace '$NAMESPACE' ..."
kubectl apply -n "$NAMESPACE" -f "$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml"
}
wait_for_openbao() {
echo "Waiting for OpenBao to become ready ..."
if kubectl get statefulset/$OPENBAO_NAME -n "$NAMESPACE" >/dev/null 2>&1; then
kubectl rollout status statefulset/$OPENBAO_NAME -n "$NAMESPACE" --timeout=120s
else
kubectl rollout status deploy/$OPENBAO_NAME -n "$NAMESPACE" --timeout=120s
fi
}
init_openbao_kv_and_store_admin_key() {
local token
token=$(cat "$root_token_file")
# Enable kv (if not already) and write keys
local svc="http://127.0.0.1:18200"
echo "Configuring KV at $svc ..."
# Check if kv/ is already mounted
if curl -sS -H "X-Vault-Token: $token" "$svc/v1/sys/mounts" | grep -q '"kv/":'; then
echo "KV path 'kv/' is already enabled."
else
curl -sS -H "X-Vault-Token: $token" -X POST "$svc/v1/sys/mounts/kv" \
-d '{"type":"kv","options":{"version":"2"}}' >/dev/null 2>&1 || true
fi
local priv pub
priv=$(base64 <"$admin_key_priv" | tr -d '\n')
pub=$(base64 <"$admin_key_pub" | tr -d '\n')
# Check if admin key already exists and matches
local existing_data
existing_data=$(curl -sS -H "X-Vault-Token: $token" "$svc/v1/kv/data/prole/admin" 2>/dev/null || true)
if [[ -n "$existing_data" ]]; then
local ex_priv ex_pub
ex_priv=$(echo "$existing_data" | grep -o '"admin_private_key_b64":"[^"]*' | cut -d'"' -f4 || true)
ex_pub=$(echo "$existing_data" | grep -o '"admin_public_key_b64":"[^"]*' | cut -d'"' -f4 || true)
if [[ "$ex_priv" == "$priv" && "$ex_pub" == "$pub" ]]; then
echo "Admin key pair in OpenBao kv/prole/admin is already up to date."
else
echo "Writing admin key pair to kv/prole/admin ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/admin" \
-d "{\"data\":{\"admin_private_key_b64\":\"$priv\",\"admin_public_key_b64\":\"$pub\"}}" >/dev/null
echo "Stored admin key pair in OpenBao kv/prole/admin."
fi
else
echo "Writing admin key pair to kv/prole/admin ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/admin" \
-d "{\"data\":{\"admin_private_key_b64\":\"$priv\",\"admin_public_key_b64\":\"$pub\"}}" >/dev/null
echo "Stored admin key pair in OpenBao kv/prole/admin."
fi
# Store User SSH keys if they exist
local user_key_priv="$HOME/.ssh/id_prole_ed25519"
local user_key_pub="$HOME/.ssh/id_prole_ed25519.pub"
if [[ -f "$user_key_priv" && -f "$user_key_pub" ]]; then
local u_priv u_pub
u_priv=$(base64 <"$user_key_priv" | tr -d '\n')
u_pub=$(base64 <"$user_key_pub" | tr -d '\n')
local username=${PROLE_DB_USER:-"prole"}
echo "Writing user SSH keys for '$username' to kv/prole/user ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/user" \
-d "{\"data\":{\"username\":\"$username\",\"private_key_b64\":\"$u_priv\",\"public_key_b64\":\"$u_pub\"}}" >/dev/null
echo "Stored user SSH keys in OpenBao kv/prole/user."
fi
if [[ -n "$db_pass" ]]; then
local username=${PROLE_DB_USER:-"prole"}
echo "Writing database user password for '$username' to kv/prole/db ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/db" \
-d "{\"data\":{\"username\":\"$username\",\"password\":\"$db_pass\"}}" >/dev/null
echo "Stored database password in OpenBao kv/prole/db."
fi
}
# Removed: prompt_admin_password_and_apply_secret (no AD admin secret required)
docker_start() {
echo "Starting local Docker container for OpenBao ..."
ensure_docker
docker rm -f "$OPENBAO_NAME" >/dev/null 2>&1 || true
# OpenBao dev
local token
token=$(cat "$root_token_file" 2>/dev/null || true)
if [[ -z "$token" ]]; then
token=$(openssl rand -hex 24)
printf "%s" "$token" >"$root_token_file"
chmod 0600 "$root_token_file"
fi
docker run -d --name "$OPENBAO_NAME" -p 18200:8200 \
"$OPENBAO_IMAGE" server -dev -dev-listen-address=0.0.0.0:8200 -dev-root-token-id="$token"
echo "Docker container started: $OPENBAO_NAME"
}
docker_stop() {
docker rm -f "$OPENBAO_NAME" >/dev/null 2>&1 || true
echo "Stopped OpenBao container if it was running."
}
docker_restart() {
docker_stop
docker_start
}
# status command implementation wrapped in a function to avoid top-level 'local'
cmd_status() {
echo "--- init_primary_domain status ---"
echo "Namespace: $NAMESPACE"
echo "OpenBao name: $OPENBAO_NAME"
echo "Manifest dir: $OPENBAO_MANIFEST_DIR"
# Files/manifests present
local ok=0
if [[ -f "$OPENBAO_MANIFEST_DIR/deployment.yaml" ]]; then
echo "[OK] OpenBao deployment manifest exists"
else
echo "[MISSING] $OPENBAO_MANIFEST_DIR/deployment.yaml"
ok=1
fi
if [[ -f "$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml" ]]; then
echo "[OK] Kerberos ConfigMap manifest exists"
else
echo "[MISSING] $OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml"
ok=1
fi
# K8s resources
if kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" >/dev/null 2>&1; then
local ready desired
ready=$(kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0")
desired=$(kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" -o jsonpath='{.status.replicas}' 2>/dev/null || echo "0")
ready=${ready:-0}
desired=${desired:-0}
if [[ "$ready" == "$desired" && "$ready" != "0" ]]; then
echo "[OK] OpenBao StatefulSet running ($ready/$desired ready)"
else
echo "[WARN] OpenBao StatefulSet not fully ready ($ready/$desired)"
ok=1
fi
elif kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" >/dev/null 2>&1; then
# Get readiness
local ready desired
ready=$(kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0")
desired=$(kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" -o jsonpath='{.status.replicas}' 2>/dev/null || echo "0")
ready=${ready:-0}
desired=${desired:-0}
if [[ "$ready" == "$desired" && "$ready" != "0" ]]; then
echo "[OK] OpenBao Deployment running ($ready/$desired ready)"
else
echo "[WARN] OpenBao Deployment not fully ready ($ready/$desired)"
ok=1
fi
else
echo "[MISSING] OpenBao Deployment or StatefulSet '$OPENBAO_NAME' in namespace '$NAMESPACE'"
ok=1
fi
if kubectl -n "$NAMESPACE" get configmap prole-krb5-conf >/dev/null 2>&1; then
echo "[OK] Kerberos ConfigMap 'prole-krb5-conf' present"
else
echo "[MISSING] Kerberos ConfigMap 'prole-krb5-conf' in namespace '$NAMESPACE'"
ok=1
fi
# Docker (optional local mode)
if command -v docker >/dev/null 2>&1; then
if docker ps --format '{{.Names}}' | grep -Fxq "$OPENBAO_NAME"; then
echo "[OK] Docker container '$OPENBAO_NAME' is running"
else
# Not necessarily an error; we primarily use k8s
echo "[INFO] Docker container '$OPENBAO_NAME' not running (k8s mode may be in use)"
fi
fi
# Secrets and tokens
if [[ -f "$admin_key_priv" && -f "$admin_key_pub" ]]; then
echo "[OK] Admin ed25519 keypair present in $SECRETS_DIR"
else
echo "[MISSING] Admin keypair files in $SECRETS_DIR"
ok=1
fi
if [[ -s "$root_token_file" ]]; then
echo "[OK] OpenBao root token file present"
else
echo "[MISSING] OpenBao root token file ($root_token_file)"
ok=1
fi
# OpenBao reachability and token validity (best-effort)
# Prefer explicit override, then localhost port-forward, then cluster DNS
local svc_url
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
svc_url="$PROLE_OPENBAO_URL"
elif curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then
svc_url="http://127.0.0.1:18200"
else
svc_url="http://$OPENBAO_NAME.$NAMESPACE.svc.cluster.local:8200"
fi
if curl -sS "$svc_url/v1/sys/health" >/dev/null 2>&1; then
echo "[OK] OpenBao service reachable"
if [[ -s "$root_token_file" ]]; then
local code
code=$(curl -s -o /dev/null -w "%{http_code}" -H "X-Vault-Token: $(cat "$root_token_file")" "$svc_url/v1/sys/mounts" || echo "000")
if [[ "$code" == "200" ]]; then
echo "[OK] OpenBao root token authenticates"
else
echo "[WARN] OpenBao root token did not authenticate (HTTP $code)"
ok=1
fi
fi
else
echo "[WARN] OpenBao service not reachable at $svc_url"
ok=1
fi
# Service certificate existence (CNPG TLS)
local CNPG_CLUSTER_NAME
CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db}
if kubectl -n "$NAMESPACE" get secret "${CNPG_CLUSTER_NAME}-tls" >/dev/null 2>&1; then
echo "[OK] Service certificate secret '${CNPG_CLUSTER_NAME}-tls' exists"
else
echo "[INFO] Service certificate secret '${CNPG_CLUSTER_NAME}-tls' not found"
fi
echo "-------------------------------------"
if [[ $ok -eq 0 ]]; then
echo "Status: OK"
return 0
else
echo "Status: Issues detected"
return 1
fi
}
case "$ACTION" in
start)
ensure_tools
ensure_docker
ensure_admin_keypair
docker_start
;;
status)
ensure_tools
cmd_status
;;
stop)
ensure_docker
docker_stop
;;
restart)
ensure_tools
ensure_docker
ensure_admin_keypair
docker_restart
;;
initialize)
ensure_tools
# Read password from stdin if provided
db_pass=""
if [[ ! -t 0 ]]; then
read -r db_pass
fi
ensure_admin_keypair
generate_openbao_manifests
generate_kerberos_configmap
apply_k8s
if [[ -n "$db_pass" ]]; then
local username=${PROLE_DB_USER:-"prole"}
echo "Creating database user secret 'prole-db-user' for '$username' ..."
kubectl create secret generic prole-db-user -n "$NAMESPACE" \
--from-literal=username="$username" \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -n "$NAMESPACE" -f -
echo "Creating database superuser secret 'prole-db-superuser' ..."
kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \
--from-literal=username=postgres \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -n "$NAMESPACE" -f -
fi
wait_for_openbao
# Ensure port-forward is running for OpenBao
echo "Ensuring port-forward for OpenBao is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart openbao
if ! curl -sS http://127.0.0.1:18200/v1/sys/health >/dev/null 2>&1; then
echo "ERROR: OpenBao not reachable at http://127.0.0.1:18200." >&2
echo "Please start port-forwards: $SCRIPT_DIR/init_port_forwards.sh start openbao" >&2
exit 1
fi
init_openbao_kv_and_store_admin_key
echo "Initialization complete. Manifests in: $OPENBAO_MANIFEST_DIR"
;;
update|reload)
generate_openbao_manifests
generate_kerberos_configmap
apply_k8s
echo "Re-applied manifests."
;;
*)
echo "Usage: $0 {start|stop|status|restart|initialize|update|reload}" >&2
exit 2
;;
esac

View File

@ -1,537 +0,0 @@
#!/usr/bin/env bash
set -u
# init_port_forwards.sh
# Portable-ish (macOS, Ubuntu, Raspberry Pi OS, Alpine) bash init-style script
# Manages kubectl port-forward daemons defined in an XML file.
PROG="init_port_forwards"
PROLE_HOME="${PROLE_HOME:-/Users/chrisfu/dev/prole}"
VERBOSE=0
CONFIG_FILE="$PROLE_HOME/conf/port-mappings.properties"
usage() {
cat <<EOF
Usage:
$PROG [-v|--verbose] [-c|--config-file=FILE] <start|stop|restart|status> [component]
Options:
-c, --config-file=FILE Path to local-ports.properties (XML)
-v, --verbose Verbose output
Examples:
$PROG -c ./port-mappings.properties start
$PROG stop openbao
$PROG --verbose status
EOF
}
TARGET_ID=""
log() { printf '%s\n' "$*"; }
vlog() { [ "$VERBOSE" -eq 1 ] && printf '[verbose] %s\n' "$*" || true; }
err() { printf '[error] %s\n' "$*" >&2; }
have() { command -v "$1" >/dev/null 2>&1; }
# Choose a writable state dir for pid/log files:
# - Prefer XDG_RUNTIME_DIR if set and writable
# - Else /var/run if writable (rare without root)
# - Else ~/.local/state
# - Else /tmp
state_dir() {
if [ -n "${XDG_RUNTIME_DIR:-}" ] && [ -w "${XDG_RUNTIME_DIR:-}" ]; then
printf '%s/%s' "$XDG_RUNTIME_DIR" "$PROG"
return
fi
if [ -d "/var/run" ] && [ -w "/var/run" ]; then
printf '/var/run/%s' "$PROG"
return
fi
if [ -n "${HOME:-}" ]; then
mkdir -p "$HOME/.local/state" >/dev/null 2>&1 || true
if [ -d "$HOME/.local/state" ] && [ -w "$HOME/.local/state" ]; then
printf '%s/.local/state/%s' "$HOME" "$PROG"
return
fi
fi
printf '/tmp/%s' "$PROG"
}
STATE_DIR="$(state_dir)"
PID_DIR="$STATE_DIR/pids"
LOG_DIR="$STATE_DIR/logs"
ensure_dirs() {
mkdir -p "$PID_DIR" "$LOG_DIR" 2>/dev/null || true
if [ ! -d "$PID_DIR" ] || [ ! -d "$LOG_DIR" ]; then
err "Unable to create state directories under: $STATE_DIR"
exit 1
fi
}
# Use kubectl for actions; kubecolor is used only for prettier status output.
KUBECTL="kubectl"
detect_kubectl() {
if have kubectl; then
KUBECTL="kubectl"
else
err "kubectl not found in PATH"
exit 1
fi
}
# Basic environment validation
validate_env() {
detect_kubectl
ensure_dirs
if ! "$KUBECTL" version --client >/dev/null 2>&1; then
err "kubectl seems broken or not executable"
exit 1
fi
# Can we reach the cluster?
if ! "$KUBECTL" cluster-info >/dev/null 2>&1; then
err "kubectl cannot reach a cluster (check KUBECONFIG/context)"
err "Try: kubectl config get-contexts && kubectl config use-context <ctx>"
exit 1
fi
}
# ---- Preflight: Docker + k3d awareness ----
# Return 0 when Docker CLI can talk to a running daemon.
docker_is_running() {
if ! have docker; then
return 1
fi
docker info >/dev/null 2>&1
}
# Hard-fail with clean guidance when Docker is not up (used for start/restart).
ensure_docker_running() {
if docker_is_running; then
return 0
fi
if ! have docker; then
err "Docker CLI not found. Please install Docker Desktop or docker CLI."
else
err "Docker is not running. Start Docker Desktop and wait until it is ready."
fi
err "Tip (macOS): open -a Docker"
err "Then re-run: $PROG start"
exit 3
}
# Detect k3d cluster name from env or current kubectl context.
# - If K3D_CLUSTER is set, use it.
# - Else, if current-context starts with 'k3d-', strip prefix to get name.
detect_k3d_context() {
if [ -n "${K3D_CLUSTER:-}" ]; then
printf '%s' "$K3D_CLUSTER"
return 0
fi
local ctx
ctx="$($KUBECTL config current-context 2>/dev/null || echo)"
case "$ctx" in
k3d-*) printf '%s' "${ctx#k3d-}"; return 0 ;;
*) return 1 ;;
esac
}
# Return 0 if the given k3d cluster exists and is running.
k3d_cluster_is_running() {
local name="$1"
have k3d || return 1
# Use json output when available; fall back to grep otherwise
if k3d cluster list -o json >/dev/null 2>&1; then
k3d cluster list -o json 2>/dev/null | grep -q '"name"\s*:\s*"'"$name"'"' && \
k3d cluster list -o json 2>/dev/null | sed -n 's/.*"name"\s*:\s*"\([^"]\+\)".*"serversRunning"\s*:\s*\([0-9]\+\).*/\1 \2/p' | awk -v n="$name" '$1==n {exit ($2>0)?0:1}'
return $?
else
k3d cluster list 2>/dev/null | grep -E "^$name\s" | grep -q running
return $?
fi
}
# If current context indicates k3d, ensure the cluster is up; otherwise no-op.
ensure_k3d_ready_if_applicable() {
local k3d_name
if ! k3d_name="$(detect_k3d_context)"; then
return 0
fi
if ! have k3d; then
err "k3d is not installed but kubectl context suggests k3d (context=$("$KUBECTL" config current-context))."
err "Install k3d: brew install k3d (macOS)"
err "Or switch context: kubectl config use-context <non-k3d-context>"
exit 4
fi
if ! k3d_cluster_is_running "$k3d_name"; then
err "k3d cluster '$k3d_name' is not running."
err "Start it: k3d cluster start $k3d_name"
err "Then re-run: $PROG start"
exit 4
fi
}
pid_file_for() { printf '%s/%s.pid' "$PID_DIR" "$1"; }
log_file_for() { printf '%s/%s.log' "$LOG_DIR" "$1"; }
is_pid_running() {
# Return 0 if pid exists and running, else 1
# kill -0 is portable.
local pid="$1"
[ -n "$pid" ] && kill -0 "$pid" >/dev/null 2>&1
}
read_pid() {
local pf="$1"
[ -f "$pf" ] || return 1
# shellcheck disable=SC2162
read pid <"$pf" || return 1
printf '%s' "$pid"
}
write_pid() {
local pf="$1" pid="$2"
printf '%s\n' "$pid" >"$pf"
}
remove_pidfile() {
local pf="$1"
rm -f "$pf" >/dev/null 2>&1 || true
}
# ---- XML parsing (simple attribute extraction) ----
# We parse lines containing: <mapping ... />
# Attributes must use double quotes in the XML (as in the sample).
get_attr() {
# $1 = line, $2 = attribute name
# outputs value or empty
printf '%s\n' "$1" | sed -n "s/.*$2=\"\([^\"]*\)\".*/\1/p"
}
foreach_mapping() {
# Calls a provided function with mapping fields:
# callback id ns target address hostPort servicePort protocol description
local callback="$1"
[ -f "$CONFIG_FILE" ] || { err "Config file not found: $CONFIG_FILE"; exit 1; }
# Support both single-line and multi-line self-closing mapping tags, e.g.:
# <mapping id="x" ... /> OR lines spanning multiple lines until "/>".
local in_mapping=0 in_comment=0 buffer="" line
while IFS= read -r line; do
# Handle XML comments: skip anything between <!-- and -->
if [ $in_comment -eq 1 ]; then
case "$line" in
*"-->"*) in_comment=0; continue ;;
*) continue ;;
esac
fi
case "$line" in
*"<!--"*)
case "$line" in
*"-->"*)
# single-line comment; skip line
continue
;;
*)
in_comment=1
continue
;;
esac
;;
esac
if [ $in_mapping -eq 0 ]; then
case "$line" in
*"<mapping"*)
in_mapping=1
buffer="$line"
;;
*)
continue
;;
esac
else
# Accumulate lines until we see the closing '/>'
buffer="$buffer $line"
fi
if [ $in_mapping -eq 1 ] && printf '%s' "$line" | grep -q "/>"; then
# Normalize whitespace to make attribute extraction robust
local merged
merged=$(printf '%s\n' "$buffer" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g')
local id ns target address hostPort servicePort protocol description
id="$(get_attr "$merged" "id")"
ns="$(get_attr "$merged" "namespace")"
target="$(get_attr "$merged" "target")"
address="$(get_attr "$merged" "address")"
hostPort="$(get_attr "$merged" "hostPort")"
servicePort="$(get_attr "$merged" "servicePort")"
protocol="$(get_attr "$merged" "protocol")"
description="$(get_attr "$merged" "description")"
# Basic validation
if [ -z "$id" ] || [ -z "$ns" ] || [ -z "$target" ] || [ -z "$hostPort" ] || [ -z "$servicePort" ]; then
err "Invalid mapping (missing required attributes): $merged"
exit 1
fi
# Filter by TARGET_ID if set
if [ -n "$TARGET_ID" ] && [ "$id" != "$TARGET_ID" ]; then
in_mapping=0
buffer=""
continue
fi
if [ -z "$address" ]; then address="127.0.0.1"; fi
if [ -z "$protocol" ]; then protocol="TCP"; fi
vlog "mapping: id=$id ns=$ns target=$target address=$address hostPort=$hostPort servicePort=$servicePort protocol=$protocol"
"$callback" "$id" "$ns" "$target" "$address" "$hostPort" "$servicePort" "$protocol" "$description"
# Reset accumulator
in_mapping=0
buffer=""
fi
done <"$CONFIG_FILE"
}
build_port_forward_cmd() {
# echo a command string
local ns="$1" target="$2" address="$3" hostPort="$4" servicePort="$5"
# kubectl port-forward -n <ns> --address <addr> <target> <local>:<remote>
printf '%s port-forward -n %s --address %s %s %s:%s' \
"$KUBECTL" "$ns" "$address" "$target" "$hostPort" "$servicePort"
}
start_port_forward() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
local pidfile logfile cmd pid
# Aggressively stop existing processes before starting to avoid port conflicts
stop_port_forward "$id" "$ns" "$target" "$address" "$hostPort" "$servicePort" "$protocol" "$description"
pidfile="$(pid_file_for "$id")"
logfile="$(log_file_for "$id")"
cmd="$(build_port_forward_cmd "$ns" "$target" "$address" "$hostPort" "$servicePort")"
log "Starting: $id $address:$hostPort -> $target:$servicePort ($ns) ${description:-}"
vlog "Command: $cmd"
# Start in background, keep output in log.
# nohup is available on macOS/Linux; redirect stdin from /dev/null to detach.
nohup sh -c "$cmd" >>"$logfile" 2>&1 </dev/null &
pid="$!"
write_pid "$pidfile" "$pid"
# Quick verification
sleep 0.2
if is_pid_running "$pid"; then
vlog "$id started (pid=$pid, log=$logfile)"
return 0
else
err "$id failed to start (see log: $logfile)"
remove_pidfile "$pidfile"
return 1
fi
}
stop_port_forward() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
local pidfile pid
pidfile="$(pid_file_for "$id")"
pid=""
if [ -f "$pidfile" ]; then
pid="$(read_pid "$pidfile" || true)"
fi
if [ -n "${pid:-}" ] && is_pid_running "$pid"; then
log "Stopping: $id (pid=$pid)"
kill "$pid" >/dev/null 2>&1 || true
# wait a moment, then SIGKILL if needed
local i
for i in 1 2 3 4 5; do
if ! is_pid_running "$pid"; then break; fi
sleep 0.2
done
if is_pid_running "$pid"; then
err "$id did not stop gracefully; sending SIGKILL"
kill -9 "$pid" >/dev/null 2>&1 || true
fi
else
if [ -f "$pidfile" ]; then
vlog "$id stale pidfile (pid=$pid not running)"
fi
fi
remove_pidfile "$pidfile"
# Aggressively remove any other matching kubectl port-forward processes
# Search for processes that match: kubectl port-forward -n <ns> ... <target> <hostPort>:<servicePort>
local extra_pids
extra_pids=$(ps -ef | grep "port-forward" | grep "\-n" | grep "$ns" | grep "$target" | grep "$hostPort:$servicePort" | grep -v grep | awk '{print $2}')
for epid in $extra_pids; do
if [ "$epid" != "$pid" ]; then
log "Cleaning up orphan process for $id (pid=$epid)"
kill -9 "$epid" >/dev/null 2>&1 || true
fi
done
}
status_one() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
local pidfile pid
pidfile="$(pid_file_for "$id")"
pid=""
if [ -f "$pidfile" ]; then
pid="$(read_pid "$pidfile" || true)"
fi
if [ -n "${pid:-}" ] && is_pid_running "$pid"; then
printf 'RUNNING %-12s pid=%-7s %s:%s -> %s:%s ns=%s %s\n' \
"$id" "$pid" "$address" "$hostPort" "$target" "$servicePort" "$ns" "${description:-}"
# ps details (portable flags vary; use a conservative format)
ps -p "$pid" -o pid=,ppid=,etime=,command= 2>/dev/null | sed 's/^/ /' || true
else
printf 'STOPPED %-12s (no live pid) %s:%s -> %s:%s ns=%s %s\n' \
"$id" "$address" "$hostPort" "$target" "$servicePort" "$ns" "${description:-}"
fi
}
do_start() {
validate_env
# Preflight: require Docker daemon and k3d (if applicable)
ensure_docker_running
ensure_k3d_ready_if_applicable
foreach_mapping start_port_forward
}
do_stop() {
# stop doesn't require cluster access, but it does need state dirs
ensure_dirs
foreach_mapping stop_port_forward
}
do_restart() {
validate_env
# Preflight: require Docker daemon and k3d (if applicable)
ensure_docker_running
ensure_k3d_ready_if_applicable
foreach_mapping stop_port_forward
foreach_mapping start_port_forward
}
do_status() {
validate_env
# Non-fatal awareness messages
if docker_is_running; then
log "[OK] Docker daemon is running"
else
if have docker; then
log "[WARN] Docker is not running"
else
log "[WARN] Docker CLI not found"
fi
fi
local _k3d_name
if _k3d_name="$(detect_k3d_context)"; then
if have k3d; then
if k3d_cluster_is_running "$_k3d_name"; then
log "[OK] k3d cluster '$_k3d_name' is running"
else
log "[WARN] k3d cluster '$_k3d_name' is not running"
fi
else
log "[WARN] k3d not installed but context suggests k3d (cluster='$_k3d_name')"
fi
fi
log "== Context / Cluster =="
"$KUBECTL" config current-context 2>/dev/null | sed 's/^/ context: /' || true
"$KUBECTL" cluster-info 2>/dev/null | sed 's/^/ /' || true
log ""
log "== Port-forward processes =="
foreach_mapping status_one
log ""
log "== Quick k3d awareness checks (best-effort) =="
# If user is on k3d, current-context often includes k3d-... but not guaranteed.
# Show nodes + a few namespaces/services related to mappings (best effort).
"$KUBECTL" get nodes -o wide 2>/dev/null | sed 's/^/ /' || true
log ""
"$KUBECTL" get ns 2>/dev/null | sed 's/^/ /' || true
log ""
# For each mapping, try to show target existence
log "== Target existence (best-effort) =="
_target_check() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
# kubectl get <target> -n <ns>
# If target is like "svc/name", "deploy/name", etc.
if "$KUBECTL" get -n "$ns" "$target" >/dev/null 2>&1; then
printf 'OK %-12s %s (ns=%s)\n' "$id" "$target" "$ns"
else
printf 'MISSING %-12s %s (ns=%s)\n' "$id" "$target" "$ns"
fi
}
foreach_mapping _target_check
}
# ---- arg parsing ----
ACTION=""
while [ $# -gt 0 ]; do
case "$1" in
start|stop|restart|status)
if [ -z "$ACTION" ]; then
ACTION="$1"
else
TARGET_ID="$1"
fi
shift
;;
-v|--verbose)
VERBOSE=1
shift
;;
-c)
shift
[ $# -gt 0 ] || { err "-c requires a file path"; usage; exit 2; }
CONFIG_FILE="$1"
shift
;;
--config-file=*)
CONFIG_FILE="${1#*=}"
shift
;;
-h|--help)
usage
exit 0
;;
*)
if [ -z "$ACTION" ]; then
err "Unknown arg: $1"
usage
exit 2
fi
TARGET_ID="$1"
shift
;;
esac
done
[ -n "$ACTION" ] || { usage; exit 2; }
case "$ACTION" in
start) do_start ;;
stop) do_stop ;;
restart) do_restart ;;
status) do_status ;;
esac

View File

@ -1,215 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# init_prole-db.sh
# Purpose:
# - Manage prole-db CloudNative-PG cluster operations (deploy, start, stop, etc.)
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ACTION=${1:-}
VERSION=${2:-latest}
# Load env
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
source "$HOME/.prole/env.sh"
fi
CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db}
NAMESPACE=${NAMESPACE:-prole}
CNPG_MANIFEST="$SCRIPT_DIR/../k8s/prole/prole-db.yaml"
ensure_tools() {
for t in kubectl jq; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
get_latest_image() {
local version_file="$SCRIPT_DIR/../conf/postgresql/.version"
if [[ -f "$version_file" ]]; then
echo "prole-db:$(cat "$version_file" | tr -d '[:space:]')"
else
echo "prole-db:17.7-033"
fi
}
start() {
ensure_tools
echo "Checking dependencies..."
# 1. k3d is running
if ! command -v k3d >/dev/null || ! k3d cluster list >/dev/null 2>&1; then
echo "ERROR: k3d is not running or not installed." >&2
exit 1
fi
# 2. cnpg operator is loaded
if ! kubectl get deployment -n cnpg-system cnpg-controller-manager >/dev/null 2>&1; then
echo "ERROR: CloudNative-PG operator is not loaded." >&2
exit 1
fi
# 3. openbao is configured
if ! kubectl get statefulset openbao -n "$NAMESPACE" >/dev/null 2>&1 && ! kubectl get deployment openbao -n "$NAMESPACE" >/dev/null 2>&1; then
echo "ERROR: OpenBao is not deployed." >&2
exit 1
fi
# Compare latest image with deployed
local latest_image
latest_image=$(get_latest_image)
local current_image
current_image=$(kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.imageName}' 2>/dev/null || echo "")
if [[ "$current_image" != "$latest_image" ]]; then
echo "Updating cluster image from '$current_image' to '$latest_image'..."
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{\"spec\": {\"imageName\": \"$latest_image\"}}"
# Force a rollout to ensure the new image is pulled even if it was just a tag update (though we use unique tags)
kubectl cnpg restart "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" || true
else
echo "Cluster is already using the latest image: $latest_image"
fi
echo "Starting prole-db cluster (ensuring manifest is applied)..."
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST"
}
stop() {
ensure_tools
echo "Stopping prole-db cluster $CNPG_CLUSTER_NAME..."
# Identify instances
local instances
instances=$(kubectl get pods -n "$NAMESPACE" -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" -o jsonpath='{.items[*].metadata.name}')
if [[ -z "$instances" ]]; then
echo "No instances found for cluster $CNPG_CLUSTER_NAME."
return
fi
# Determine replicas and primary
local primary
primary=$(kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" -o jsonpath='{.status.currentPrimary}')
echo "Primary: $primary"
# Shutdown replicas first
for pod in $instances; do
if [[ "$pod" != "$primary" ]]; then
echo "Shutting down replica $pod..."
kubectl exec -n "$NAMESPACE" "$pod" -c postgres -- psql -U postgres -c "CHECKPOINT;" || true
fi
done
# Shutdown primary last
if [[ -n "$primary" ]]; then
echo "Shutting down primary $primary..."
kubectl exec -n "$NAMESPACE" "$primary" -c postgres -- psql -U postgres -c "CHECKPOINT;" || true
fi
echo "Deleting cluster resource..."
kubectl delete -f "$CNPG_MANIFEST" --ignore-not-found
}
restart() {
ensure_tools
echo "Restarting prole-db cluster..."
kubectl cnpg restart "$CNPG_CLUSTER_NAME" -n "$NAMESPACE"
}
deploy() {
ensure_tools
local image
if [[ "$VERSION" == "latest" ]]; then
image=$(get_latest_image)
else
image="prole-db:$VERSION"
fi
echo "Deploying $image to cluster $CNPG_CLUSTER_NAME..."
# If cluster doesn't exist, use init_cloudnative_pg.sh create first
if ! kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" >/dev/null 2>&1; then
echo "Cluster not found. Running init_cloudnative_pg.sh create..."
bash "$SCRIPT_DIR/init_cloudnative_pg.sh" create
fi
# Ensure image is updated if manifest has an older version
# First, apply the manifest to ensure the cluster exists/is updated
echo "Applying manifest $CNPG_MANIFEST..."
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST"
# Then force the specific image version via patch if different
local current_image
current_image=$(kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.imageName}' 2>/dev/null || echo "")
if [[ "$current_image" != "$image" ]]; then
echo "Patching cluster to use image '$image' (was '$current_image')..."
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{\"spec\": {\"imageName\": \"$image\"}}"
# Force a rollout
kubectl cnpg restart "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" || true
fi
}
rollback() {
ensure_tools
echo "Rollback: Updating to previous image (manually specified version or fallback)..."
if [[ "$VERSION" == "latest" ]]; then
echo "Please specify a version to rollback to. Usage: $0 rollback <version>"
exit 1
fi
deploy
}
backup() {
echo "Backup - tbd, when we configure s3 or other block store"
}
reset() {
ensure_tools
echo "Resetting prole-db cluster..."
echo "Removing cnpg instances and pods..."
kubectl delete cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --ignore-not-found
# Wait for deletion
kubectl wait --for=delete cluster/"$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --timeout=60s || true
echo "Reinstantiating..."
deploy
}
case "$ACTION" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
deploy)
deploy
;;
rollback)
rollback
;;
backup)
backup
;;
reset)
reset
;;
*)
echo "Usage: $0 {start|stop|restart|deploy|rollback|backup|reset} [version]" >&2
exit 2
;;
esac

View File

@ -1,29 +0,0 @@
#!/usr/bin/env bash
# Prole environment configuration
# This file is generated by the installer. Source it in new shells, or execute as a wrapper:
# "$PROLE_HOME/env.sh" <command> [args…]
# shellcheck shell=bash
export PROLE_HOME="<MagicMock name='mock.Entry().get().strip()' id='4413171632'>"
export PROLE_CONF="<MagicMock name='mock.Entry().get().strip()' id='4413171632'>"
export PROLE_DATA="<MagicMock name='mock.Entry().get().strip()' id='4413171632'>"
export PROLE_LOGS="<MagicMock name='mock.Entry().get().strip()' id='4413171632'>"
export PROLE_SERVICE="<MagicMock name='mock.Entry().get().strip()' id='4413171632'>"
# Ensure PATH works for GUI-launched shells (Docker, Ollama, etc.)
_prole_add_path() { case ":${PATH}:" in *":$1:"*) ;; *) PATH="$1:${PATH:-}" ;; esac; }
_prole_add_path "$PROLE_HOME/bin"
_prole_add_path "/opt/homebrew/bin"
_prole_add_path "/usr/local/bin"
_prole_add_path "/usr/bin"
_prole_add_path "/bin"
_prole_add_path "/usr/sbin"
_prole_add_path "/sbin"
export PATH
# Add custom paths below if needed (examples):
# _prole_add_path "/Applications/Ollama.app/Contents/MacOS"
# If executed with arguments, run them under this environment
if [ "$#" -gt 0 ]; then
exec "$@"
fi

View File

@ -1,15 +0,0 @@
myrddin.prole.org 10.0.0.3
raspberry.prole.org 10.0.0.4
pi.prole.org 10.0.0.5
synology.prole.org 10.0.0.203
morgoth.prole.org 10.0.0.204
zinfandel.prole.org 10.0.0.205
aventage.prole.org 10.0.0.206
retropie.prole.org 10.0.0.207
fairyland.prole.org 10.0.0.208
k8s.prole.org zinfandel.prole.org
mc.prole.org 73.15.20.166
morana.prole.org 10.0.0.66
ollama.prole.org 73.15.20.166
svc.prole.org 73.15.20.166
www.prole.org ghs.googlehosted.com

View File

@ -1,326 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# init_cloudnative_pg.sh
# Purpose:
# - Distribute administrator ed25519 key pair to CloudNativePG as a Kubernetes Secret for cert auth
# - Configure Kerberos (GSSAPI) using external Kerberos KDC
# - Patch CNPG cluster to enable TLS and GSSAPI where possible
#
# Usage:
# ./init_cloudnative_pg.sh start|stop|status|restart
# ./init_cloudnative_pg.sh initialize # create/update k8s secrets/configs and patch CNPG
# ./init_cloudnative_pg.sh update|reload # re-apply/patch
#
# Requirements:
# - init_openbao.sh has been run (OpenBao running in k8s)
# - $PROLE_HOME/env.sh or $HOME/.prole/env.sh defining PROLE_SERVICE
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Load env without leaking our positional args to the env script (some env.sh may `exec "$@"`).
__PROLE_SAVED_ARGS=("$@")
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$HOME/.prole/env.sh"
else
echo "ERROR: Missing env. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
exit 1
fi
set -- "${__PROLE_SAVED_ARGS[@]}"
unset __PROLE_SAVED_ARGS
if [[ -z "${PROLE_SERVICE:-}" ]]; then
echo "ERROR: PROLE_SERVICE is not defined in env." >&2
exit 1
fi
ACTION=${1:-}
CNPG_CLUSTER_NAME=${2:-${CNPG_CLUSTER_NAME:-prole-db}}
NAMESPACE=${NAMESPACE:-prole}
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
REALM=${REALM:-PROLE.ORG}
DOMAIN=${DOMAIN:-prole.org}
KRB5_KDC=${KRB5_KDC:-}
KRB5_ADMIN=${KRB5_ADMIN:-}
CNPG_MANIFEST="$SCRIPT_DIR/../k8s/prole/prole-db.yaml"
SECRETS_DIR="$PROLE_SERVICE/secrets"
ADMIN_PRIV="$SECRETS_DIR/admin_ed25519.key"
ADMIN_PUB="$SECRETS_DIR/admin_ed25519.pub"
OPENBAO_TOKEN_FILE="$SECRETS_DIR/openbao-root-token"
ensure_tools() {
for t in kubectl curl openssl base64 jq; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
# Resolve OpenBao URL: prefer explicit env, then localhost port-forward, then cluster DNS
bao_service_url() {
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
echo "$PROLE_OPENBAO_URL"
return 0
fi
# Prefer standard local port-forward managed by etc/init_port_forwards.sh
if curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:18200"
return 0
fi
echo "http://$OPENBAO_NAME.$NAMESPACE.svc.cluster.local:8200"
}
fetch_admin_keys_and_db_pass_from_bao_or_local() {
local token url
if [[ -f "$OPENBAO_TOKEN_FILE" ]]; then
token=$(cat "$OPENBAO_TOKEN_FILE")
else
token=""
fi
url=$(bao_service_url)
if [[ -n "$token" ]]; then
echo "Attempting to read admin key pair from OpenBao kv/prole/admin ..."
if curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -e '.data.data' >/dev/null 2>&1; then
local priv_b64 pub_b64
priv_b64=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -r '.data.data.admin_private_key_b64')
pub_b64=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -r '.data.data.admin_public_key_b64')
printf "%s" "$priv_b64" | base64 -d >"$ADMIN_PRIV"
printf "%s" "$pub_b64" | base64 -d >"$ADMIN_PUB"
chmod 0600 "$ADMIN_PRIV"
fi
echo "Attempting to read database password from OpenBao kv/prole/db ..."
if curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/db" | jq -e '.data.data' >/dev/null 2>&1; then
local db_pass
db_pass=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/db" | jq -r '.data.data.password')
if [[ -n "$db_pass" ]]; then
echo "Updating database user secret 'prole-db-user' from OpenBao ..."
kubectl create secret generic prole-db-user -n "$NAMESPACE" \
--from-literal=username=prole \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -f -
echo "Updating database superuser secret 'prole-db-superuser' from OpenBao ..."
kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \
--from-literal=username=postgres \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -f -
fi
fi
fi
if [[ -f "$ADMIN_PRIV" && -f "$ADMIN_PUB" ]]; then
echo "Using local admin key pair at $SECRETS_DIR"
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() {
echo "Creating/updating Secret cnpg-admin-key ..."
kubectl create secret generic cnpg-admin-key -n "$NAMESPACE" \
--from-file=admin.key="$ADMIN_PRIV" \
--from-file=admin.pub="$ADMIN_PUB" \
--dry-run=client -o yaml | kubectl apply -f -
}
ensure_krb5_conf_configmap() {
echo "Creating/updating ConfigMap prole-krb5-conf ..."
local kdc_val admin_val
if [[ -n "$KRB5_KDC" ]]; then
kdc_val="$KRB5_KDC"
admin_val=${KRB5_ADMIN:-$(echo "$KRB5_KDC" | cut -d, -f1)}
else
kdc_val="kdc.$DOMAIN"
admin_val="kdc.$DOMAIN"
fi
local TMP
TMP=$(mktemp)
cat >"$TMP" <<EOF
[libdefaults]
default_realm = $REALM
dns_lookup_realm = true
dns_lookup_kdc = true
[realms]
$REALM = {
kdc = $kdc_val
admin_server = $admin_val
}
[domain_realm]
.$DOMAIN = $REALM
$DOMAIN = $REALM
EOF
kubectl -n "$NAMESPACE" create configmap prole-krb5-conf --from-file=krb5.conf="$TMP" --dry-run=client -o yaml | kubectl apply -f -
rm -f "$TMP"
}
generate_tls_if_missing() {
local ca_secret_name="${CNPG_CLUSTER_NAME}-ca"
if kubectl -n "$NAMESPACE" get secret "$ca_secret_name" >/dev/null 2>&1; then
echo "CA secret $ca_secret_name already exists; skipping generation."
return 0
fi
echo "Generating self-signed CA (RSA 4096) for CNPG ..."
local TMPD
TMPD=$(mktemp -d)
openssl genrsa -out "$TMPD/ca.key" 4096
openssl req -x509 -new -key "$TMPD/ca.key" -out "$TMPD/ca.crt" -days 3650 -subj "/CN=Prole CNPG CA"
kubectl -n "$NAMESPACE" create secret generic "$ca_secret_name" \
--from-file=ca.crt="$TMPD/ca.crt" \
--from-file=ca.key="$TMPD/ca.key" \
--dry-run=client -o yaml | kubectl apply -f -
rm -rf "$TMPD"
}
patch_cnpg_cluster_for_auth() {
echo "Patching CNPG Cluster $CNPG_CLUSTER_NAME to enable GSSAPI and fix config (best-effort) ..."
# Best-effort patch; schema may vary with CNPG version.
# We remove krb_srvname as it is unrecognized in some PG 17 builds.
# We revert certificates to default to avoid operator TLS issues.
kubectl -n "$NAMESPACE" patch cluster "$CNPG_CLUSTER_NAME" --type merge -p "{
\"spec\": {
\"postgresql\": {
\"parameters\": {\"krb_srvname\": null},
\"pg_hba\": [
\"local all postgres trust\",
\"host all postgres all scram-sha-256\",
\"host all all all gss include_realm=1 krb_realm=$REALM\",
\"host all all all scram-sha-256\"
]
},
\"certificates\": {
\"serverCASecret\": null,
\"clientCASecret\": null
}
}
}" >/dev/null || echo "Note: patch may need adjustment for your CNPG version."
}
initialize() {
ensure_tools
# Ensure port-forward is running for OpenBao (dependency)
echo "Ensuring port-forward for OpenBao is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart openbao
fetch_admin_keys_and_db_pass_from_bao_or_local
apply_cnpg_admin_secret
ensure_krb5_conf_configmap
# generate_tls_if_missing
patch_cnpg_cluster_for_auth
# Ensure port-forward is running for Postgres (local access)
echo "Ensuring port-forward for Postgres is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart postgres
echo "Initialization complete for CNPG + Kerberos + cert artifacts."
}
update_reload() {
initialize
}
case "$ACTION" in
recreate)
ensure_tools
"$0" delete "$CNPG_CLUSTER_NAME"
"$0" create "$CNPG_CLUSTER_NAME"
;;
create)
ensure_tools
echo "Installing CloudNative-PG operator ..."
kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.27/releases/cnpg-1.27.0.yaml
echo "Creating CloudNative-PG cluster and resources for '$CNPG_CLUSTER_NAME' ..."
kubectl apply -k "$SCRIPT_DIR/../k8s/prole"
# Ensure prole-index-html exists for prole deployment readiness probe
if ! kubectl get configmap prole-index-html -n prole >/dev/null 2>&1; then
echo "Creating prole-index-html configmap..."
printf "<html><body><h1>Prole</h1></body></html>" > /tmp/index.html
kubectl create configmap prole-index-html --from-file=/tmp/index.html -n prole
rm /tmp/index.html
fi
# Ensure prole-nginx-tls exists (self-signed for dev)
if ! kubectl get secret prole-nginx-tls -n prole >/dev/null 2>&1; then
echo "Generating self-signed prole-nginx-tls for development..."
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /tmp/nginx-tls.key -out /tmp/nginx-tls.crt \
-subj "/CN=prole.org" >/dev/null 2>&1
kubectl create secret tls prole-nginx-tls --key /tmp/nginx-tls.key --cert /tmp/nginx-tls.crt -n prole
rm /tmp/nginx-tls.key /tmp/nginx-tls.crt
fi
echo "Initializing and patching cluster ..."
initialize
;;
delete)
ensure_tools
echo "Deleting all resources for '$CNPG_CLUSTER_NAME' ..."
kubectl delete -k "$SCRIPT_DIR/../k8s/prole" --ignore-not-found
;;
start)
ensure_tools
if [[ ! -f "$CNPG_MANIFEST" ]]; then
echo "ERROR: CNPG manifest not found at $CNPG_MANIFEST" >&2
exit 1
fi
echo "Starting CloudNative-PG cluster from $CNPG_MANIFEST in namespace $NAMESPACE..."
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST"
;;
stop)
ensure_tools
if [[ ! -f "$CNPG_MANIFEST" ]]; then
echo "ERROR: CNPG manifest not found at $CNPG_MANIFEST" >&2
exit 1
fi
echo "Stopping CloudNative-PG cluster using $CNPG_MANIFEST ..."
kubectl delete -f "$CNPG_MANIFEST" --ignore-not-found
;;
status)
ensure_tools
echo "--- CloudNative-PG Cluster Status ($CNPG_CLUSTER_NAME) ---"
if kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" >/dev/null 2>&1; then
kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME"
echo ""
echo "CNPG Plugin Status:"
if kubectl cnpg version >/dev/null 2>&1; then
kubectl cnpg status "$CNPG_CLUSTER_NAME" -n "$NAMESPACE"
else
echo "Note: 'kubectl cnpg' plugin not found; skipping detailed status."
fi
else
echo "Cluster '$CNPG_CLUSTER_NAME' not found in namespace '$NAMESPACE'."
fi
;;
restart)
ensure_tools
"$0" stop
"$0" start
;;
initialize)
initialize
;;
update|reload)
update_reload
;;
*)
echo "Usage: $0 {create|delete|recreate|start|stop|status|restart|initialize|update|reload} [dbname]" >&2
exit 2
;;
esac

View File

@ -1,203 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# etc/init_k8s.sh
# Purpose: Manage k3d/k8s environment
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Preserve positional args while sourcing env
__PROLE_SAVED_ARGS=("$@")
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$HOME/.prole/env.sh"
fi
set -- "${__PROLE_SAVED_ARGS[@]}"
unset __PROLE_SAVED_ARGS
# Default values
VERBOSE=false
ENVIRONMENT="prod"
HOST=""
CLUSTER_NAME_DEFAULT="prole-dev-cluster"
CLUSTER_PORT_DEFAULT="6443"
usage() {
cat <<EOF
Usage: $0 [initialize|start|stop|restart|status] [options]
Actions:
initialize Create a k3d cluster (dev) or fetch kubeconfig (service/prod)
start Start the k3d cluster
stop Stop the k3d cluster
restart Restart the k3d cluster
status Show status of Docker and k3d cluster
Options:
-v, --verbose Enable verbose output
-e, --environment dev|service|prod (default: $ENVIRONMENT)
-h, --host Remote host for service/prod environments
-n, --name k3d cluster name
EOF
exit 1
}
log() {
echo "[INFO] $*"
}
debug() {
if [[ "$VERBOSE" == "true" ]]; then
echo "[DEBUG] $*"
fi
}
# Parse options
POSITIONAL_ARGS=()
CLUSTER_NAME=""
while [[ $# -gt 0 ]]; do
case $1 in
-v|--verbose)
VERBOSE=true
shift
;;
-e|--environment)
ENVIRONMENT="$2"
shift 2
;;
-h|--host)
HOST="$2"
shift 2
;;
-n|--name)
CLUSTER_NAME="$2"
shift 2
;;
*)
POSITIONAL_ARGS+=("$1")
shift
;;
esac
done
set -- "${POSITIONAL_ARGS[@]}"
ACTION=${1:-}
if [[ -z "$ACTION" ]]; then
usage
fi
ensure_tools() {
for t in k3d docker; do
command -v "$t" >/dev/null || { echo "ERROR: Missing required tool: $t" >&2; exit 1; }
done
}
ensure_docker() {
if ! docker info >/dev/null 2>&1; then
echo "ERROR: Docker is not running." >&2
exit 1
fi
}
initialize() {
ensure_tools
ensure_docker
if [[ "$ENVIRONMENT" == "dev" ]]; then
if [[ -z "$CLUSTER_NAME" ]]; then
if [[ -t 0 ]]; then
read -p "Enter k3d cluster name [$CLUSTER_NAME_DEFAULT]: " CLUSTER_NAME
CLUSTER_NAME=${CLUSTER_NAME:-$CLUSTER_NAME_DEFAULT}
else
CLUSTER_NAME=$CLUSTER_NAME_DEFAULT
fi
fi
if k3d cluster list "$CLUSTER_NAME" >/dev/null 2>&1; then
log "Cluster '$CLUSTER_NAME' already exists."
else
log "Creating k3d cluster '$CLUSTER_NAME'..."
k3d cluster create "$CLUSTER_NAME" -p "0.0.0.0:$CLUSTER_PORT_DEFAULT:6443@server:0"
fi
else
if [[ -n "$HOST" ]]; then
log "Environment is $ENVIRONMENT. Host is $HOST."
log "TODO: Fetch kubeconfig from $HOST (placeholder)."
else
log "Environment is $ENVIRONMENT. Use -h|--host to specify the remote cluster host."
fi
fi
}
status() {
ensure_tools
log "Checking Docker status..."
if docker info >/dev/null 2>&1; then
echo "Docker is running."
else
echo "Docker is NOT running."
fi
log "Listing k3d clusters..."
k3d cluster list
}
get_cluster_name() {
# If a name was provided or exists in env, use it.
# Otherwise, if there is only one cluster, use it.
# Finally, use default.
if [[ -n "${CLUSTER_NAME:-}" ]]; then
echo "$CLUSTER_NAME"
return
fi
local clusters
clusters=$(k3d cluster list --no-headers | awk '{print $1}')
local count
count=$(echo "$clusters" | grep -c . || true)
if [[ "$count" -eq 1 ]]; then
echo "$clusters"
else
echo "$CLUSTER_NAME_DEFAULT"
fi
}
case "$ACTION" in
initialize)
initialize
;;
status)
status
;;
start)
ensure_tools
CLUSTER_NAME=$(get_cluster_name)
log "Starting k3d cluster '$CLUSTER_NAME'..."
k3d cluster start "$CLUSTER_NAME"
;;
stop)
ensure_tools
CLUSTER_NAME=$(get_cluster_name)
log "Stopping k3d cluster '$CLUSTER_NAME'..."
k3d cluster stop "$CLUSTER_NAME"
;;
restart)
ensure_tools
CLUSTER_NAME=$(get_cluster_name)
log "Restarting k3d cluster '$CLUSTER_NAME'..."
k3d cluster stop "$CLUSTER_NAME"
k3d cluster start "$CLUSTER_NAME"
;;
*)
usage
;;
esac

View File

@ -1,490 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# init_openbao.sh
# Purpose:
# - Deploy OpenBao to Kubernetes (dev mode) and store admin ed25519 key pair
# Generate and apply a Kerberos krb5.conf ConfigMap for an external realm
# - Local Docker helpers for OpenBao (optional)
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Preserve positional args while sourcing env
__PROLE_SAVED_ARGS=("$@")
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$HOME/.prole/env.sh"
fi
set -- "${__PROLE_SAVED_ARGS[@]}"
unset __PROLE_SAVED_ARGS
if [[ -z "${PROLE_SERVICE:-}" ]]; then
echo "ERROR: PROLE_SERVICE is not defined in env. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
exit 1
fi
ACTION=${1:-}
# Defaults
NAMESPACE=${NAMESPACE:-prole}
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
OPENBAO_MANIFEST_DIR="$SCRIPT_DIR/../k8s/openbao"
# Kerberos realm defaults
KRB5_REALM=${KRB5_REALM:-PROLE.ORG}
DOMAIN=${DOMAIN:-$(echo "$KRB5_REALM" | tr 'A-Z' 'a-z')}
KRB5_KDC=${KRB5_KDC:-kdc.$DOMAIN}
KRB5_ADMIN=${KRB5_ADMIN:-}
# Secrets and token locations
SECRETS_DIR="$PROLE_SERVICE/secrets"
mkdir -p "$SECRETS_DIR"
admin_key_priv="$SECRETS_DIR/admin_ed25519.key"
admin_key_pub="$SECRETS_DIR/admin_ed25519.pub"
root_token_file="$SECRETS_DIR/openbao-root-token"
ensure_tools() {
for t in kubectl curl openssl base64; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
ensure_docker() {
command -v docker >/dev/null || { echo "Missing required tool: docker" >&2; exit 1; }
}
ensure_admin_keypair() {
# Ensure admin ed25519 keypair exists and a root token is available
mkdir -p "$SECRETS_DIR"
if [[ ! -f "$admin_key_priv" || ! -f "$admin_key_pub" ]]; then
echo "Generating admin ed25519 keypair in $SECRETS_DIR ..."
openssl genpkey -algorithm ED25519 -out "$admin_key_priv"
openssl pkey -in "$admin_key_priv" -pubout -out "$admin_key_pub"
chmod 0600 "$admin_key_priv"
fi
# Ensure a root token exists for OpenBao dev server interactions
if [[ ! -f "$root_token_file" || ! -s "$root_token_file" ]]; then
openssl rand -hex 24 >"$root_token_file"
chmod 0600 "$root_token_file"
fi
}
generate_openbao_manifests() {
mkdir -p "$OPENBAO_MANIFEST_DIR"
# Only create a simple deployment if none exists; otherwise respect current file
local deploy="$OPENBAO_MANIFEST_DIR/deployment.yaml"
if [[ ! -f "$deploy" ]]; then
cat >"$deploy" <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: $OPENBAO_NAME
namespace: $NAMESPACE
spec:
replicas: 1
selector:
matchLabels:
app: $OPENBAO_NAME
template:
metadata:
labels:
app: $OPENBAO_NAME
spec:
containers:
- name: $OPENBAO_NAME
image: $OPENBAO_IMAGE
args: ["server","-dev","-dev-listen-address=0.0.0.0:8200"]
ports:
- containerPort: 8200
---
apiVersion: v1
kind: Service
metadata:
name: $OPENBAO_NAME
namespace: $NAMESPACE
spec:
selector:
app: $OPENBAO_NAME
ports:
- name: http
port: 8200
targetPort: 8200
EOF
fi
}
generate_kerberos_configmap() {
mkdir -p "$OPENBAO_MANIFEST_DIR"
local domain_lower
domain_lower=$(echo "$DOMAIN" | tr 'A-Z' 'a-z')
# Split KRB5_KDC on comma into bash array
local IFS=','
read -r -a __kdcs <<< "$KRB5_KDC"
local kdc_lines=""
local kdc
for kdc in "${__kdcs[@]}"; do
kdc_lines+=$' kdc = '"$kdc"$'\n'
done
local admin_server admin_line
admin_server=${KRB5_ADMIN:-"${__kdcs[0]:-}"}
admin_line=""
if [[ -n "$admin_server" ]]; then
# 8 spaces to remain within the YAML literal block indentation
admin_line=$' admin_server = '"$admin_server"$'\n'
fi
cat >"$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml" <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: prole-krb5-conf
namespace: $NAMESPACE
data:
krb5.conf: |
[libdefaults]
default_realm = $KRB5_REALM
dns_lookup_realm = true
dns_lookup_kdc = true
[realms]
$KRB5_REALM = {
${kdc_lines}${admin_line} }
[domain_realm]
.$domain_lower = $KRB5_REALM
$domain_lower = $KRB5_REALM
EOF
}
apply_k8s() {
echo "Applying OpenBao manifest to namespace '$NAMESPACE' ..."
if [[ -f "$SCRIPT_DIR/../k8s/prole/openbao-statefulset.yaml" ]]; then
kubectl apply -n "$NAMESPACE" -f "$SCRIPT_DIR/../k8s/prole/openbao-statefulset.yaml"
kubectl apply -n "$NAMESPACE" -f "$SCRIPT_DIR/../k8s/prole/openbao-service.yaml"
else
kubectl apply -n "$NAMESPACE" -f "$OPENBAO_MANIFEST_DIR/deployment.yaml"
fi
echo "Applying Kerberos ConfigMap (external realm) to namespace '$NAMESPACE' ..."
kubectl apply -n "$NAMESPACE" -f "$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml"
}
wait_for_openbao() {
echo "Waiting for OpenBao to become ready ..."
if kubectl get statefulset/$OPENBAO_NAME -n "$NAMESPACE" >/dev/null 2>&1; then
kubectl rollout status statefulset/$OPENBAO_NAME -n "$NAMESPACE" --timeout=120s
else
kubectl rollout status deploy/$OPENBAO_NAME -n "$NAMESPACE" --timeout=120s
fi
}
init_openbao_kv_and_store_admin_key() {
local token
token=$(cat "$root_token_file")
# Enable kv (if not already) and write keys
local svc="http://127.0.0.1:18200"
echo "Configuring KV at $svc ..."
# Check if kv/ is already mounted
if curl -sS -H "X-Vault-Token: $token" "$svc/v1/sys/mounts" | grep -q '"kv/":'; then
echo "KV path 'kv/' is already enabled."
else
curl -sS -H "X-Vault-Token: $token" -X POST "$svc/v1/sys/mounts/kv" \
-d '{"type":"kv","options":{"version":"2"}}' >/dev/null 2>&1 || true
fi
local priv pub
priv=$(base64 <"$admin_key_priv" | tr -d '\n')
pub=$(base64 <"$admin_key_pub" | tr -d '\n')
# Check if admin key already exists and matches
local existing_data
existing_data=$(curl -sS -H "X-Vault-Token: $token" "$svc/v1/kv/data/prole/admin" 2>/dev/null || true)
if [[ -n "$existing_data" ]]; then
local ex_priv ex_pub
ex_priv=$(echo "$existing_data" | grep -o '"admin_private_key_b64":"[^"]*' | cut -d'"' -f4 || true)
ex_pub=$(echo "$existing_data" | grep -o '"admin_public_key_b64":"[^"]*' | cut -d'"' -f4 || true)
if [[ "$ex_priv" == "$priv" && "$ex_pub" == "$pub" ]]; then
echo "Admin key pair in OpenBao kv/prole/admin is already up to date."
else
echo "Writing admin key pair to kv/prole/admin ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/admin" \
-d "{\"data\":{\"admin_private_key_b64\":\"$priv\",\"admin_public_key_b64\":\"$pub\"}}" >/dev/null
echo "Stored admin key pair in OpenBao kv/prole/admin."
fi
else
echo "Writing admin key pair to kv/prole/admin ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/admin" \
-d "{\"data\":{\"admin_private_key_b64\":\"$priv\",\"admin_public_key_b64\":\"$pub\"}}" >/dev/null
echo "Stored admin key pair in OpenBao kv/prole/admin."
fi
# Store User SSH keys if they exist
local user_key_priv="$HOME/.ssh/id_prole_ed25519"
local user_key_pub="$HOME/.ssh/id_prole_ed25519.pub"
if [[ -f "$user_key_priv" && -f "$user_key_pub" ]]; then
local u_priv u_pub
u_priv=$(base64 <"$user_key_priv" | tr -d '\n')
u_pub=$(base64 <"$user_key_pub" | tr -d '\n')
local username=${PROLE_DB_USER:-"prole"}
echo "Writing user SSH keys for '$username' to kv/prole/user ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/user" \
-d "{\"data\":{\"username\":\"$username\",\"private_key_b64\":\"$u_priv\",\"public_key_b64\":\"$u_pub\"}}" >/dev/null
echo "Stored user SSH keys in OpenBao kv/prole/user."
fi
if [[ -n "$db_pass" ]]; then
local username=${PROLE_DB_USER:-"prole"}
echo "Writing database user password for '$username' to kv/prole/db ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/db" \
-d "{\"data\":{\"username\":\"$username\",\"password\":\"$db_pass\"}}" >/dev/null
echo "Stored database password in OpenBao kv/prole/db."
fi
}
# Removed: prompt_admin_password_and_apply_secret (no AD admin secret required)
docker_start() {
echo "Starting local Docker container for OpenBao ..."
ensure_docker
docker rm -f "$OPENBAO_NAME" >/dev/null 2>&1 || true
# OpenBao dev
local token
token=$(cat "$root_token_file" 2>/dev/null || true)
if [[ -z "$token" ]]; then
token=$(openssl rand -hex 24)
printf "%s" "$token" >"$root_token_file"
chmod 0600 "$root_token_file"
fi
docker run -d --name "$OPENBAO_NAME" -p 18200:8200 \
"$OPENBAO_IMAGE" server -dev -dev-listen-address=0.0.0.0:8200 -dev-root-token-id="$token"
echo "Docker container started: $OPENBAO_NAME"
}
docker_stop() {
docker rm -f "$OPENBAO_NAME" >/dev/null 2>&1 || true
echo "Stopped OpenBao container if it was running."
}
docker_restart() {
docker_stop
docker_start
}
# status command implementation wrapped in a function to avoid top-level 'local'
cmd_status() {
echo "--- init_primary_domain status ---"
echo "Namespace: $NAMESPACE"
echo "OpenBao name: $OPENBAO_NAME"
echo "Manifest dir: $OPENBAO_MANIFEST_DIR"
# Files/manifests present
local ok=0
if [[ -f "$OPENBAO_MANIFEST_DIR/deployment.yaml" ]]; then
echo "[OK] OpenBao deployment manifest exists"
else
echo "[MISSING] $OPENBAO_MANIFEST_DIR/deployment.yaml"
ok=1
fi
if [[ -f "$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml" ]]; then
echo "[OK] Kerberos ConfigMap manifest exists"
else
echo "[MISSING] $OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml"
ok=1
fi
# K8s resources
if kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" >/dev/null 2>&1; then
local ready desired
ready=$(kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0")
desired=$(kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" -o jsonpath='{.status.replicas}' 2>/dev/null || echo "0")
ready=${ready:-0}
desired=${desired:-0}
if [[ "$ready" == "$desired" && "$ready" != "0" ]]; then
echo "[OK] OpenBao StatefulSet running ($ready/$desired ready)"
else
echo "[WARN] OpenBao StatefulSet not fully ready ($ready/$desired)"
ok=1
fi
elif kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" >/dev/null 2>&1; then
# Get readiness
local ready desired
ready=$(kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0")
desired=$(kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" -o jsonpath='{.status.replicas}' 2>/dev/null || echo "0")
ready=${ready:-0}
desired=${desired:-0}
if [[ "$ready" == "$desired" && "$ready" != "0" ]]; then
echo "[OK] OpenBao Deployment running ($ready/$desired ready)"
else
echo "[WARN] OpenBao Deployment not fully ready ($ready/$desired)"
ok=1
fi
else
echo "[MISSING] OpenBao Deployment or StatefulSet '$OPENBAO_NAME' in namespace '$NAMESPACE'"
ok=1
fi
if kubectl -n "$NAMESPACE" get configmap prole-krb5-conf >/dev/null 2>&1; then
echo "[OK] Kerberos ConfigMap 'prole-krb5-conf' present"
else
echo "[MISSING] Kerberos ConfigMap 'prole-krb5-conf' in namespace '$NAMESPACE'"
ok=1
fi
# Docker (optional local mode)
if command -v docker >/dev/null 2>&1; then
if docker ps --format '{{.Names}}' | grep -Fxq "$OPENBAO_NAME"; then
echo "[OK] Docker container '$OPENBAO_NAME' is running"
else
# Not necessarily an error; we primarily use k8s
echo "[INFO] Docker container '$OPENBAO_NAME' not running (k8s mode may be in use)"
fi
fi
# Secrets and tokens
if [[ -f "$admin_key_priv" && -f "$admin_key_pub" ]]; then
echo "[OK] Admin ed25519 keypair present in $SECRETS_DIR"
else
echo "[MISSING] Admin keypair files in $SECRETS_DIR"
ok=1
fi
if [[ -s "$root_token_file" ]]; then
echo "[OK] OpenBao root token file present"
else
echo "[MISSING] OpenBao root token file ($root_token_file)"
ok=1
fi
# OpenBao reachability and token validity (best-effort)
# Prefer explicit override, then localhost port-forward, then cluster DNS
local svc_url
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
svc_url="$PROLE_OPENBAO_URL"
elif curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then
svc_url="http://127.0.0.1:18200"
else
svc_url="http://$OPENBAO_NAME.$NAMESPACE.svc.cluster.local:8200"
fi
if curl -sS "$svc_url/v1/sys/health" >/dev/null 2>&1; then
echo "[OK] OpenBao service reachable"
if [[ -s "$root_token_file" ]]; then
local code
code=$(curl -s -o /dev/null -w "%{http_code}" -H "X-Vault-Token: $(cat "$root_token_file")" "$svc_url/v1/sys/mounts" || echo "000")
if [[ "$code" == "200" ]]; then
echo "[OK] OpenBao root token authenticates"
else
echo "[WARN] OpenBao root token did not authenticate (HTTP $code)"
ok=1
fi
fi
else
echo "[WARN] OpenBao service not reachable at $svc_url"
ok=1
fi
# Service certificate existence (CNPG TLS)
local CNPG_CLUSTER_NAME
CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db}
if kubectl -n "$NAMESPACE" get secret "${CNPG_CLUSTER_NAME}-tls" >/dev/null 2>&1; then
echo "[OK] Service certificate secret '${CNPG_CLUSTER_NAME}-tls' exists"
else
echo "[INFO] Service certificate secret '${CNPG_CLUSTER_NAME}-tls' not found"
fi
echo "-------------------------------------"
if [[ $ok -eq 0 ]]; then
echo "Status: OK"
return 0
else
echo "Status: Issues detected"
return 1
fi
}
case "$ACTION" in
start)
ensure_tools
ensure_docker
ensure_admin_keypair
docker_start
;;
status)
ensure_tools
cmd_status
;;
stop)
ensure_docker
docker_stop
;;
restart)
ensure_tools
ensure_docker
ensure_admin_keypair
docker_restart
;;
initialize)
ensure_tools
# Read password from stdin if provided
db_pass=""
if [[ ! -t 0 ]]; then
read -r db_pass
fi
ensure_admin_keypair
generate_openbao_manifests
generate_kerberos_configmap
apply_k8s
if [[ -n "$db_pass" ]]; then
local username=${PROLE_DB_USER:-"prole"}
echo "Creating database user secret 'prole-db-user' for '$username' ..."
kubectl create secret generic prole-db-user -n "$NAMESPACE" \
--from-literal=username="$username" \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -n "$NAMESPACE" -f -
echo "Creating database superuser secret 'prole-db-superuser' ..."
kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \
--from-literal=username=postgres \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -n "$NAMESPACE" -f -
fi
wait_for_openbao
# Ensure port-forward is running for OpenBao
echo "Ensuring port-forward for OpenBao is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart openbao
if ! curl -sS http://127.0.0.1:18200/v1/sys/health >/dev/null 2>&1; then
echo "ERROR: OpenBao not reachable at http://127.0.0.1:18200." >&2
echo "Please start port-forwards: $SCRIPT_DIR/init_port_forwards.sh start openbao" >&2
exit 1
fi
init_openbao_kv_and_store_admin_key
echo "Initialization complete. Manifests in: $OPENBAO_MANIFEST_DIR"
;;
update|reload)
generate_openbao_manifests
generate_kerberos_configmap
apply_k8s
echo "Re-applied manifests."
;;
*)
echo "Usage: $0 {start|stop|status|restart|initialize|update|reload}" >&2
exit 2
;;
esac

View File

@ -1,537 +0,0 @@
#!/usr/bin/env bash
set -u
# init_port_forwards.sh
# Portable-ish (macOS, Ubuntu, Raspberry Pi OS, Alpine) bash init-style script
# Manages kubectl port-forward daemons defined in an XML file.
PROG="init_port_forwards"
PROLE_HOME="${PROLE_HOME:-/Users/chrisfu/dev/prole}"
VERBOSE=0
CONFIG_FILE="$PROLE_HOME/conf/port-mappings.properties"
usage() {
cat <<EOF
Usage:
$PROG [-v|--verbose] [-c|--config-file=FILE] <start|stop|restart|status> [component]
Options:
-c, --config-file=FILE Path to local-ports.properties (XML)
-v, --verbose Verbose output
Examples:
$PROG -c ./port-mappings.properties start
$PROG stop openbao
$PROG --verbose status
EOF
}
TARGET_ID=""
log() { printf '%s\n' "$*"; }
vlog() { [ "$VERBOSE" -eq 1 ] && printf '[verbose] %s\n' "$*" || true; }
err() { printf '[error] %s\n' "$*" >&2; }
have() { command -v "$1" >/dev/null 2>&1; }
# Choose a writable state dir for pid/log files:
# - Prefer XDG_RUNTIME_DIR if set and writable
# - Else /var/run if writable (rare without root)
# - Else ~/.local/state
# - Else /tmp
state_dir() {
if [ -n "${XDG_RUNTIME_DIR:-}" ] && [ -w "${XDG_RUNTIME_DIR:-}" ]; then
printf '%s/%s' "$XDG_RUNTIME_DIR" "$PROG"
return
fi
if [ -d "/var/run" ] && [ -w "/var/run" ]; then
printf '/var/run/%s' "$PROG"
return
fi
if [ -n "${HOME:-}" ]; then
mkdir -p "$HOME/.local/state" >/dev/null 2>&1 || true
if [ -d "$HOME/.local/state" ] && [ -w "$HOME/.local/state" ]; then
printf '%s/.local/state/%s' "$HOME" "$PROG"
return
fi
fi
printf '/tmp/%s' "$PROG"
}
STATE_DIR="$(state_dir)"
PID_DIR="$STATE_DIR/pids"
LOG_DIR="$STATE_DIR/logs"
ensure_dirs() {
mkdir -p "$PID_DIR" "$LOG_DIR" 2>/dev/null || true
if [ ! -d "$PID_DIR" ] || [ ! -d "$LOG_DIR" ]; then
err "Unable to create state directories under: $STATE_DIR"
exit 1
fi
}
# Use kubectl for actions; kubecolor is used only for prettier status output.
KUBECTL="kubectl"
detect_kubectl() {
if have kubectl; then
KUBECTL="kubectl"
else
err "kubectl not found in PATH"
exit 1
fi
}
# Basic environment validation
validate_env() {
detect_kubectl
ensure_dirs
if ! "$KUBECTL" version --client >/dev/null 2>&1; then
err "kubectl seems broken or not executable"
exit 1
fi
# Can we reach the cluster?
if ! "$KUBECTL" cluster-info >/dev/null 2>&1; then
err "kubectl cannot reach a cluster (check KUBECONFIG/context)"
err "Try: kubectl config get-contexts && kubectl config use-context <ctx>"
exit 1
fi
}
# ---- Preflight: Docker + k3d awareness ----
# Return 0 when Docker CLI can talk to a running daemon.
docker_is_running() {
if ! have docker; then
return 1
fi
docker info >/dev/null 2>&1
}
# Hard-fail with clean guidance when Docker is not up (used for start/restart).
ensure_docker_running() {
if docker_is_running; then
return 0
fi
if ! have docker; then
err "Docker CLI not found. Please install Docker Desktop or docker CLI."
else
err "Docker is not running. Start Docker Desktop and wait until it is ready."
fi
err "Tip (macOS): open -a Docker"
err "Then re-run: $PROG start"
exit 3
}
# Detect k3d cluster name from env or current kubectl context.
# - If K3D_CLUSTER is set, use it.
# - Else, if current-context starts with 'k3d-', strip prefix to get name.
detect_k3d_context() {
if [ -n "${K3D_CLUSTER:-}" ]; then
printf '%s' "$K3D_CLUSTER"
return 0
fi
local ctx
ctx="$($KUBECTL config current-context 2>/dev/null || echo)"
case "$ctx" in
k3d-*) printf '%s' "${ctx#k3d-}"; return 0 ;;
*) return 1 ;;
esac
}
# Return 0 if the given k3d cluster exists and is running.
k3d_cluster_is_running() {
local name="$1"
have k3d || return 1
# Use json output when available; fall back to grep otherwise
if k3d cluster list -o json >/dev/null 2>&1; then
k3d cluster list -o json 2>/dev/null | grep -q '"name"\s*:\s*"'"$name"'"' && \
k3d cluster list -o json 2>/dev/null | sed -n 's/.*"name"\s*:\s*"\([^"]\+\)".*"serversRunning"\s*:\s*\([0-9]\+\).*/\1 \2/p' | awk -v n="$name" '$1==n {exit ($2>0)?0:1}'
return $?
else
k3d cluster list 2>/dev/null | grep -E "^$name\s" | grep -q running
return $?
fi
}
# If current context indicates k3d, ensure the cluster is up; otherwise no-op.
ensure_k3d_ready_if_applicable() {
local k3d_name
if ! k3d_name="$(detect_k3d_context)"; then
return 0
fi
if ! have k3d; then
err "k3d is not installed but kubectl context suggests k3d (context=$("$KUBECTL" config current-context))."
err "Install k3d: brew install k3d (macOS)"
err "Or switch context: kubectl config use-context <non-k3d-context>"
exit 4
fi
if ! k3d_cluster_is_running "$k3d_name"; then
err "k3d cluster '$k3d_name' is not running."
err "Start it: k3d cluster start $k3d_name"
err "Then re-run: $PROG start"
exit 4
fi
}
pid_file_for() { printf '%s/%s.pid' "$PID_DIR" "$1"; }
log_file_for() { printf '%s/%s.log' "$LOG_DIR" "$1"; }
is_pid_running() {
# Return 0 if pid exists and running, else 1
# kill -0 is portable.
local pid="$1"
[ -n "$pid" ] && kill -0 "$pid" >/dev/null 2>&1
}
read_pid() {
local pf="$1"
[ -f "$pf" ] || return 1
# shellcheck disable=SC2162
read pid <"$pf" || return 1
printf '%s' "$pid"
}
write_pid() {
local pf="$1" pid="$2"
printf '%s\n' "$pid" >"$pf"
}
remove_pidfile() {
local pf="$1"
rm -f "$pf" >/dev/null 2>&1 || true
}
# ---- XML parsing (simple attribute extraction) ----
# We parse lines containing: <mapping ... />
# Attributes must use double quotes in the XML (as in the sample).
get_attr() {
# $1 = line, $2 = attribute name
# outputs value or empty
printf '%s\n' "$1" | sed -n "s/.*$2=\"\([^\"]*\)\".*/\1/p"
}
foreach_mapping() {
# Calls a provided function with mapping fields:
# callback id ns target address hostPort servicePort protocol description
local callback="$1"
[ -f "$CONFIG_FILE" ] || { err "Config file not found: $CONFIG_FILE"; exit 1; }
# Support both single-line and multi-line self-closing mapping tags, e.g.:
# <mapping id="x" ... /> OR lines spanning multiple lines until "/>".
local in_mapping=0 in_comment=0 buffer="" line
while IFS= read -r line; do
# Handle XML comments: skip anything between <!-- and -->
if [ $in_comment -eq 1 ]; then
case "$line" in
*"-->"*) in_comment=0; continue ;;
*) continue ;;
esac
fi
case "$line" in
*"<!--"*)
case "$line" in
*"-->"*)
# single-line comment; skip line
continue
;;
*)
in_comment=1
continue
;;
esac
;;
esac
if [ $in_mapping -eq 0 ]; then
case "$line" in
*"<mapping"*)
in_mapping=1
buffer="$line"
;;
*)
continue
;;
esac
else
# Accumulate lines until we see the closing '/>'
buffer="$buffer $line"
fi
if [ $in_mapping -eq 1 ] && printf '%s' "$line" | grep -q "/>"; then
# Normalize whitespace to make attribute extraction robust
local merged
merged=$(printf '%s\n' "$buffer" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g')
local id ns target address hostPort servicePort protocol description
id="$(get_attr "$merged" "id")"
ns="$(get_attr "$merged" "namespace")"
target="$(get_attr "$merged" "target")"
address="$(get_attr "$merged" "address")"
hostPort="$(get_attr "$merged" "hostPort")"
servicePort="$(get_attr "$merged" "servicePort")"
protocol="$(get_attr "$merged" "protocol")"
description="$(get_attr "$merged" "description")"
# Basic validation
if [ -z "$id" ] || [ -z "$ns" ] || [ -z "$target" ] || [ -z "$hostPort" ] || [ -z "$servicePort" ]; then
err "Invalid mapping (missing required attributes): $merged"
exit 1
fi
# Filter by TARGET_ID if set
if [ -n "$TARGET_ID" ] && [ "$id" != "$TARGET_ID" ]; then
in_mapping=0
buffer=""
continue
fi
if [ -z "$address" ]; then address="127.0.0.1"; fi
if [ -z "$protocol" ]; then protocol="TCP"; fi
vlog "mapping: id=$id ns=$ns target=$target address=$address hostPort=$hostPort servicePort=$servicePort protocol=$protocol"
"$callback" "$id" "$ns" "$target" "$address" "$hostPort" "$servicePort" "$protocol" "$description"
# Reset accumulator
in_mapping=0
buffer=""
fi
done <"$CONFIG_FILE"
}
build_port_forward_cmd() {
# echo a command string
local ns="$1" target="$2" address="$3" hostPort="$4" servicePort="$5"
# kubectl port-forward -n <ns> --address <addr> <target> <local>:<remote>
printf '%s port-forward -n %s --address %s %s %s:%s' \
"$KUBECTL" "$ns" "$address" "$target" "$hostPort" "$servicePort"
}
start_port_forward() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
local pidfile logfile cmd pid
# Aggressively stop existing processes before starting to avoid port conflicts
stop_port_forward "$id" "$ns" "$target" "$address" "$hostPort" "$servicePort" "$protocol" "$description"
pidfile="$(pid_file_for "$id")"
logfile="$(log_file_for "$id")"
cmd="$(build_port_forward_cmd "$ns" "$target" "$address" "$hostPort" "$servicePort")"
log "Starting: $id $address:$hostPort -> $target:$servicePort ($ns) ${description:-}"
vlog "Command: $cmd"
# Start in background, keep output in log.
# nohup is available on macOS/Linux; redirect stdin from /dev/null to detach.
nohup sh -c "$cmd" >>"$logfile" 2>&1 </dev/null &
pid="$!"
write_pid "$pidfile" "$pid"
# Quick verification
sleep 0.2
if is_pid_running "$pid"; then
vlog "$id started (pid=$pid, log=$logfile)"
return 0
else
err "$id failed to start (see log: $logfile)"
remove_pidfile "$pidfile"
return 1
fi
}
stop_port_forward() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
local pidfile pid
pidfile="$(pid_file_for "$id")"
pid=""
if [ -f "$pidfile" ]; then
pid="$(read_pid "$pidfile" || true)"
fi
if [ -n "${pid:-}" ] && is_pid_running "$pid"; then
log "Stopping: $id (pid=$pid)"
kill "$pid" >/dev/null 2>&1 || true
# wait a moment, then SIGKILL if needed
local i
for i in 1 2 3 4 5; do
if ! is_pid_running "$pid"; then break; fi
sleep 0.2
done
if is_pid_running "$pid"; then
err "$id did not stop gracefully; sending SIGKILL"
kill -9 "$pid" >/dev/null 2>&1 || true
fi
else
if [ -f "$pidfile" ]; then
vlog "$id stale pidfile (pid=$pid not running)"
fi
fi
remove_pidfile "$pidfile"
# Aggressively remove any other matching kubectl port-forward processes
# Search for processes that match: kubectl port-forward -n <ns> ... <target> <hostPort>:<servicePort>
local extra_pids
extra_pids=$(ps -ef | grep "port-forward" | grep "\-n" | grep "$ns" | grep "$target" | grep "$hostPort:$servicePort" | grep -v grep | awk '{print $2}')
for epid in $extra_pids; do
if [ "$epid" != "$pid" ]; then
log "Cleaning up orphan process for $id (pid=$epid)"
kill -9 "$epid" >/dev/null 2>&1 || true
fi
done
}
status_one() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
local pidfile pid
pidfile="$(pid_file_for "$id")"
pid=""
if [ -f "$pidfile" ]; then
pid="$(read_pid "$pidfile" || true)"
fi
if [ -n "${pid:-}" ] && is_pid_running "$pid"; then
printf 'RUNNING %-12s pid=%-7s %s:%s -> %s:%s ns=%s %s\n' \
"$id" "$pid" "$address" "$hostPort" "$target" "$servicePort" "$ns" "${description:-}"
# ps details (portable flags vary; use a conservative format)
ps -p "$pid" -o pid=,ppid=,etime=,command= 2>/dev/null | sed 's/^/ /' || true
else
printf 'STOPPED %-12s (no live pid) %s:%s -> %s:%s ns=%s %s\n' \
"$id" "$address" "$hostPort" "$target" "$servicePort" "$ns" "${description:-}"
fi
}
do_start() {
validate_env
# Preflight: require Docker daemon and k3d (if applicable)
ensure_docker_running
ensure_k3d_ready_if_applicable
foreach_mapping start_port_forward
}
do_stop() {
# stop doesn't require cluster access, but it does need state dirs
ensure_dirs
foreach_mapping stop_port_forward
}
do_restart() {
validate_env
# Preflight: require Docker daemon and k3d (if applicable)
ensure_docker_running
ensure_k3d_ready_if_applicable
foreach_mapping stop_port_forward
foreach_mapping start_port_forward
}
do_status() {
validate_env
# Non-fatal awareness messages
if docker_is_running; then
log "[OK] Docker daemon is running"
else
if have docker; then
log "[WARN] Docker is not running"
else
log "[WARN] Docker CLI not found"
fi
fi
local _k3d_name
if _k3d_name="$(detect_k3d_context)"; then
if have k3d; then
if k3d_cluster_is_running "$_k3d_name"; then
log "[OK] k3d cluster '$_k3d_name' is running"
else
log "[WARN] k3d cluster '$_k3d_name' is not running"
fi
else
log "[WARN] k3d not installed but context suggests k3d (cluster='$_k3d_name')"
fi
fi
log "== Context / Cluster =="
"$KUBECTL" config current-context 2>/dev/null | sed 's/^/ context: /' || true
"$KUBECTL" cluster-info 2>/dev/null | sed 's/^/ /' || true
log ""
log "== Port-forward processes =="
foreach_mapping status_one
log ""
log "== Quick k3d awareness checks (best-effort) =="
# If user is on k3d, current-context often includes k3d-... but not guaranteed.
# Show nodes + a few namespaces/services related to mappings (best effort).
"$KUBECTL" get nodes -o wide 2>/dev/null | sed 's/^/ /' || true
log ""
"$KUBECTL" get ns 2>/dev/null | sed 's/^/ /' || true
log ""
# For each mapping, try to show target existence
log "== Target existence (best-effort) =="
_target_check() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
# kubectl get <target> -n <ns>
# If target is like "svc/name", "deploy/name", etc.
if "$KUBECTL" get -n "$ns" "$target" >/dev/null 2>&1; then
printf 'OK %-12s %s (ns=%s)\n' "$id" "$target" "$ns"
else
printf 'MISSING %-12s %s (ns=%s)\n' "$id" "$target" "$ns"
fi
}
foreach_mapping _target_check
}
# ---- arg parsing ----
ACTION=""
while [ $# -gt 0 ]; do
case "$1" in
start|stop|restart|status)
if [ -z "$ACTION" ]; then
ACTION="$1"
else
TARGET_ID="$1"
fi
shift
;;
-v|--verbose)
VERBOSE=1
shift
;;
-c)
shift
[ $# -gt 0 ] || { err "-c requires a file path"; usage; exit 2; }
CONFIG_FILE="$1"
shift
;;
--config-file=*)
CONFIG_FILE="${1#*=}"
shift
;;
-h|--help)
usage
exit 0
;;
*)
if [ -z "$ACTION" ]; then
err "Unknown arg: $1"
usage
exit 2
fi
TARGET_ID="$1"
shift
;;
esac
done
[ -n "$ACTION" ] || { usage; exit 2; }
case "$ACTION" in
start) do_start ;;
stop) do_stop ;;
restart) do_restart ;;
status) do_status ;;
esac

View File

@ -1,215 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# init_prole-db.sh
# Purpose:
# - Manage prole-db CloudNative-PG cluster operations (deploy, start, stop, etc.)
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ACTION=${1:-}
VERSION=${2:-latest}
# Load env
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
source "$HOME/.prole/env.sh"
fi
CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db}
NAMESPACE=${NAMESPACE:-prole}
CNPG_MANIFEST="$SCRIPT_DIR/../k8s/prole/prole-db.yaml"
ensure_tools() {
for t in kubectl jq; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
get_latest_image() {
local version_file="$SCRIPT_DIR/../conf/postgresql/.version"
if [[ -f "$version_file" ]]; then
echo "prole-db:$(cat "$version_file" | tr -d '[:space:]')"
else
echo "prole-db:17.7-033"
fi
}
start() {
ensure_tools
echo "Checking dependencies..."
# 1. k3d is running
if ! command -v k3d >/dev/null || ! k3d cluster list >/dev/null 2>&1; then
echo "ERROR: k3d is not running or not installed." >&2
exit 1
fi
# 2. cnpg operator is loaded
if ! kubectl get deployment -n cnpg-system cnpg-controller-manager >/dev/null 2>&1; then
echo "ERROR: CloudNative-PG operator is not loaded." >&2
exit 1
fi
# 3. openbao is configured
if ! kubectl get statefulset openbao -n "$NAMESPACE" >/dev/null 2>&1 && ! kubectl get deployment openbao -n "$NAMESPACE" >/dev/null 2>&1; then
echo "ERROR: OpenBao is not deployed." >&2
exit 1
fi
# Compare latest image with deployed
local latest_image
latest_image=$(get_latest_image)
local current_image
current_image=$(kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.imageName}' 2>/dev/null || echo "")
if [[ "$current_image" != "$latest_image" ]]; then
echo "Updating cluster image from '$current_image' to '$latest_image'..."
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{\"spec\": {\"imageName\": \"$latest_image\"}}"
# Force a rollout to ensure the new image is pulled even if it was just a tag update (though we use unique tags)
kubectl cnpg restart "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" || true
else
echo "Cluster is already using the latest image: $latest_image"
fi
echo "Starting prole-db cluster (ensuring manifest is applied)..."
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST"
}
stop() {
ensure_tools
echo "Stopping prole-db cluster $CNPG_CLUSTER_NAME..."
# Identify instances
local instances
instances=$(kubectl get pods -n "$NAMESPACE" -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" -o jsonpath='{.items[*].metadata.name}')
if [[ -z "$instances" ]]; then
echo "No instances found for cluster $CNPG_CLUSTER_NAME."
return
fi
# Determine replicas and primary
local primary
primary=$(kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" -o jsonpath='{.status.currentPrimary}')
echo "Primary: $primary"
# Shutdown replicas first
for pod in $instances; do
if [[ "$pod" != "$primary" ]]; then
echo "Shutting down replica $pod..."
kubectl exec -n "$NAMESPACE" "$pod" -c postgres -- psql -U postgres -c "CHECKPOINT;" || true
fi
done
# Shutdown primary last
if [[ -n "$primary" ]]; then
echo "Shutting down primary $primary..."
kubectl exec -n "$NAMESPACE" "$primary" -c postgres -- psql -U postgres -c "CHECKPOINT;" || true
fi
echo "Deleting cluster resource..."
kubectl delete -f "$CNPG_MANIFEST" --ignore-not-found
}
restart() {
ensure_tools
echo "Restarting prole-db cluster..."
kubectl cnpg restart "$CNPG_CLUSTER_NAME" -n "$NAMESPACE"
}
deploy() {
ensure_tools
local image
if [[ "$VERSION" == "latest" ]]; then
image=$(get_latest_image)
else
image="prole-db:$VERSION"
fi
echo "Deploying $image to cluster $CNPG_CLUSTER_NAME..."
# If cluster doesn't exist, use init_cloudnative_pg.sh create first
if ! kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" >/dev/null 2>&1; then
echo "Cluster not found. Running init_cloudnative_pg.sh create..."
bash "$SCRIPT_DIR/init_cloudnative_pg.sh" create
fi
# Ensure image is updated if manifest has an older version
# First, apply the manifest to ensure the cluster exists/is updated
echo "Applying manifest $CNPG_MANIFEST..."
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST"
# Then force the specific image version via patch if different
local current_image
current_image=$(kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.imageName}' 2>/dev/null || echo "")
if [[ "$current_image" != "$image" ]]; then
echo "Patching cluster to use image '$image' (was '$current_image')..."
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{\"spec\": {\"imageName\": \"$image\"}}"
# Force a rollout
kubectl cnpg restart "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" || true
fi
}
rollback() {
ensure_tools
echo "Rollback: Updating to previous image (manually specified version or fallback)..."
if [[ "$VERSION" == "latest" ]]; then
echo "Please specify a version to rollback to. Usage: $0 rollback <version>"
exit 1
fi
deploy
}
backup() {
echo "Backup - tbd, when we configure s3 or other block store"
}
reset() {
ensure_tools
echo "Resetting prole-db cluster..."
echo "Removing cnpg instances and pods..."
kubectl delete cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --ignore-not-found
# Wait for deletion
kubectl wait --for=delete cluster/"$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --timeout=60s || true
echo "Reinstantiating..."
deploy
}
case "$ACTION" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
deploy)
deploy
;;
rollback)
rollback
;;
backup)
backup
;;
reset)
reset
;;
*)
echo "Usage: $0 {start|stop|restart|deploy|rollback|backup|reset} [version]" >&2
exit 2
;;
esac

View File

@ -1,29 +0,0 @@
#!/usr/bin/env bash
# Prole environment configuration
# This file is generated by the installer. Source it in new shells, or execute as a wrapper:
# "$PROLE_HOME/env.sh" <command> [args…]
# shellcheck shell=bash
export PROLE_HOME="<MagicMock name='mock.Entry().get().strip()' id='4452427696'>"
export PROLE_CONF="<MagicMock name='mock.Entry().get().strip()' id='4452427696'>"
export PROLE_DATA="<MagicMock name='mock.Entry().get().strip()' id='4452427696'>"
export PROLE_LOGS="<MagicMock name='mock.Entry().get().strip()' id='4452427696'>"
export PROLE_SERVICE="<MagicMock name='mock.Entry().get().strip()' id='4452427696'>"
# Ensure PATH works for GUI-launched shells (Docker, Ollama, etc.)
_prole_add_path() { case ":${PATH}:" in *":$1:"*) ;; *) PATH="$1:${PATH:-}" ;; esac; }
_prole_add_path "$PROLE_HOME/bin"
_prole_add_path "/opt/homebrew/bin"
_prole_add_path "/usr/local/bin"
_prole_add_path "/usr/bin"
_prole_add_path "/bin"
_prole_add_path "/usr/sbin"
_prole_add_path "/sbin"
export PATH
# Add custom paths below if needed (examples):
# _prole_add_path "/Applications/Ollama.app/Contents/MacOS"
# If executed with arguments, run them under this environment
if [ "$#" -gt 0 ]; then
exec "$@"
fi

View File

@ -1,15 +0,0 @@
myrddin.prole.org 10.0.0.3
raspberry.prole.org 10.0.0.4
pi.prole.org 10.0.0.5
synology.prole.org 10.0.0.203
morgoth.prole.org 10.0.0.204
zinfandel.prole.org 10.0.0.205
aventage.prole.org 10.0.0.206
retropie.prole.org 10.0.0.207
fairyland.prole.org 10.0.0.208
k8s.prole.org zinfandel.prole.org
mc.prole.org 73.15.20.166
morana.prole.org 10.0.0.66
ollama.prole.org 73.15.20.166
svc.prole.org 73.15.20.166
www.prole.org ghs.googlehosted.com

View File

@ -1,326 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# init_cloudnative_pg.sh
# Purpose:
# - Distribute administrator ed25519 key pair to CloudNativePG as a Kubernetes Secret for cert auth
# - Configure Kerberos (GSSAPI) using external Kerberos KDC
# - Patch CNPG cluster to enable TLS and GSSAPI where possible
#
# Usage:
# ./init_cloudnative_pg.sh start|stop|status|restart
# ./init_cloudnative_pg.sh initialize # create/update k8s secrets/configs and patch CNPG
# ./init_cloudnative_pg.sh update|reload # re-apply/patch
#
# Requirements:
# - init_openbao.sh has been run (OpenBao running in k8s)
# - $PROLE_HOME/env.sh or $HOME/.prole/env.sh defining PROLE_SERVICE
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Load env without leaking our positional args to the env script (some env.sh may `exec "$@"`).
__PROLE_SAVED_ARGS=("$@")
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$HOME/.prole/env.sh"
else
echo "ERROR: Missing env. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
exit 1
fi
set -- "${__PROLE_SAVED_ARGS[@]}"
unset __PROLE_SAVED_ARGS
if [[ -z "${PROLE_SERVICE:-}" ]]; then
echo "ERROR: PROLE_SERVICE is not defined in env." >&2
exit 1
fi
ACTION=${1:-}
CNPG_CLUSTER_NAME=${2:-${CNPG_CLUSTER_NAME:-prole-db}}
NAMESPACE=${NAMESPACE:-prole}
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
REALM=${REALM:-PROLE.ORG}
DOMAIN=${DOMAIN:-prole.org}
KRB5_KDC=${KRB5_KDC:-}
KRB5_ADMIN=${KRB5_ADMIN:-}
CNPG_MANIFEST="$SCRIPT_DIR/../k8s/prole/prole-db.yaml"
SECRETS_DIR="$PROLE_SERVICE/secrets"
ADMIN_PRIV="$SECRETS_DIR/admin_ed25519.key"
ADMIN_PUB="$SECRETS_DIR/admin_ed25519.pub"
OPENBAO_TOKEN_FILE="$SECRETS_DIR/openbao-root-token"
ensure_tools() {
for t in kubectl curl openssl base64 jq; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
# Resolve OpenBao URL: prefer explicit env, then localhost port-forward, then cluster DNS
bao_service_url() {
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
echo "$PROLE_OPENBAO_URL"
return 0
fi
# Prefer standard local port-forward managed by etc/init_port_forwards.sh
if curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:18200"
return 0
fi
echo "http://$OPENBAO_NAME.$NAMESPACE.svc.cluster.local:8200"
}
fetch_admin_keys_and_db_pass_from_bao_or_local() {
local token url
if [[ -f "$OPENBAO_TOKEN_FILE" ]]; then
token=$(cat "$OPENBAO_TOKEN_FILE")
else
token=""
fi
url=$(bao_service_url)
if [[ -n "$token" ]]; then
echo "Attempting to read admin key pair from OpenBao kv/prole/admin ..."
if curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -e '.data.data' >/dev/null 2>&1; then
local priv_b64 pub_b64
priv_b64=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -r '.data.data.admin_private_key_b64')
pub_b64=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -r '.data.data.admin_public_key_b64')
printf "%s" "$priv_b64" | base64 -d >"$ADMIN_PRIV"
printf "%s" "$pub_b64" | base64 -d >"$ADMIN_PUB"
chmod 0600 "$ADMIN_PRIV"
fi
echo "Attempting to read database password from OpenBao kv/prole/db ..."
if curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/db" | jq -e '.data.data' >/dev/null 2>&1; then
local db_pass
db_pass=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/db" | jq -r '.data.data.password')
if [[ -n "$db_pass" ]]; then
echo "Updating database user secret 'prole-db-user' from OpenBao ..."
kubectl create secret generic prole-db-user -n "$NAMESPACE" \
--from-literal=username=prole \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -f -
echo "Updating database superuser secret 'prole-db-superuser' from OpenBao ..."
kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \
--from-literal=username=postgres \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -f -
fi
fi
fi
if [[ -f "$ADMIN_PRIV" && -f "$ADMIN_PUB" ]]; then
echo "Using local admin key pair at $SECRETS_DIR"
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() {
echo "Creating/updating Secret cnpg-admin-key ..."
kubectl create secret generic cnpg-admin-key -n "$NAMESPACE" \
--from-file=admin.key="$ADMIN_PRIV" \
--from-file=admin.pub="$ADMIN_PUB" \
--dry-run=client -o yaml | kubectl apply -f -
}
ensure_krb5_conf_configmap() {
echo "Creating/updating ConfigMap prole-krb5-conf ..."
local kdc_val admin_val
if [[ -n "$KRB5_KDC" ]]; then
kdc_val="$KRB5_KDC"
admin_val=${KRB5_ADMIN:-$(echo "$KRB5_KDC" | cut -d, -f1)}
else
kdc_val="kdc.$DOMAIN"
admin_val="kdc.$DOMAIN"
fi
local TMP
TMP=$(mktemp)
cat >"$TMP" <<EOF
[libdefaults]
default_realm = $REALM
dns_lookup_realm = true
dns_lookup_kdc = true
[realms]
$REALM = {
kdc = $kdc_val
admin_server = $admin_val
}
[domain_realm]
.$DOMAIN = $REALM
$DOMAIN = $REALM
EOF
kubectl -n "$NAMESPACE" create configmap prole-krb5-conf --from-file=krb5.conf="$TMP" --dry-run=client -o yaml | kubectl apply -f -
rm -f "$TMP"
}
generate_tls_if_missing() {
local ca_secret_name="${CNPG_CLUSTER_NAME}-ca"
if kubectl -n "$NAMESPACE" get secret "$ca_secret_name" >/dev/null 2>&1; then
echo "CA secret $ca_secret_name already exists; skipping generation."
return 0
fi
echo "Generating self-signed CA (RSA 4096) for CNPG ..."
local TMPD
TMPD=$(mktemp -d)
openssl genrsa -out "$TMPD/ca.key" 4096
openssl req -x509 -new -key "$TMPD/ca.key" -out "$TMPD/ca.crt" -days 3650 -subj "/CN=Prole CNPG CA"
kubectl -n "$NAMESPACE" create secret generic "$ca_secret_name" \
--from-file=ca.crt="$TMPD/ca.crt" \
--from-file=ca.key="$TMPD/ca.key" \
--dry-run=client -o yaml | kubectl apply -f -
rm -rf "$TMPD"
}
patch_cnpg_cluster_for_auth() {
echo "Patching CNPG Cluster $CNPG_CLUSTER_NAME to enable GSSAPI and fix config (best-effort) ..."
# Best-effort patch; schema may vary with CNPG version.
# We remove krb_srvname as it is unrecognized in some PG 17 builds.
# We revert certificates to default to avoid operator TLS issues.
kubectl -n "$NAMESPACE" patch cluster "$CNPG_CLUSTER_NAME" --type merge -p "{
\"spec\": {
\"postgresql\": {
\"parameters\": {\"krb_srvname\": null},
\"pg_hba\": [
\"local all postgres trust\",
\"host all postgres all scram-sha-256\",
\"host all all all gss include_realm=1 krb_realm=$REALM\",
\"host all all all scram-sha-256\"
]
},
\"certificates\": {
\"serverCASecret\": null,
\"clientCASecret\": null
}
}
}" >/dev/null || echo "Note: patch may need adjustment for your CNPG version."
}
initialize() {
ensure_tools
# Ensure port-forward is running for OpenBao (dependency)
echo "Ensuring port-forward for OpenBao is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart openbao
fetch_admin_keys_and_db_pass_from_bao_or_local
apply_cnpg_admin_secret
ensure_krb5_conf_configmap
# generate_tls_if_missing
patch_cnpg_cluster_for_auth
# Ensure port-forward is running for Postgres (local access)
echo "Ensuring port-forward for Postgres is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart postgres
echo "Initialization complete for CNPG + Kerberos + cert artifacts."
}
update_reload() {
initialize
}
case "$ACTION" in
recreate)
ensure_tools
"$0" delete "$CNPG_CLUSTER_NAME"
"$0" create "$CNPG_CLUSTER_NAME"
;;
create)
ensure_tools
echo "Installing CloudNative-PG operator ..."
kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.27/releases/cnpg-1.27.0.yaml
echo "Creating CloudNative-PG cluster and resources for '$CNPG_CLUSTER_NAME' ..."
kubectl apply -k "$SCRIPT_DIR/../k8s/prole"
# Ensure prole-index-html exists for prole deployment readiness probe
if ! kubectl get configmap prole-index-html -n prole >/dev/null 2>&1; then
echo "Creating prole-index-html configmap..."
printf "<html><body><h1>Prole</h1></body></html>" > /tmp/index.html
kubectl create configmap prole-index-html --from-file=/tmp/index.html -n prole
rm /tmp/index.html
fi
# Ensure prole-nginx-tls exists (self-signed for dev)
if ! kubectl get secret prole-nginx-tls -n prole >/dev/null 2>&1; then
echo "Generating self-signed prole-nginx-tls for development..."
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /tmp/nginx-tls.key -out /tmp/nginx-tls.crt \
-subj "/CN=prole.org" >/dev/null 2>&1
kubectl create secret tls prole-nginx-tls --key /tmp/nginx-tls.key --cert /tmp/nginx-tls.crt -n prole
rm /tmp/nginx-tls.key /tmp/nginx-tls.crt
fi
echo "Initializing and patching cluster ..."
initialize
;;
delete)
ensure_tools
echo "Deleting all resources for '$CNPG_CLUSTER_NAME' ..."
kubectl delete -k "$SCRIPT_DIR/../k8s/prole" --ignore-not-found
;;
start)
ensure_tools
if [[ ! -f "$CNPG_MANIFEST" ]]; then
echo "ERROR: CNPG manifest not found at $CNPG_MANIFEST" >&2
exit 1
fi
echo "Starting CloudNative-PG cluster from $CNPG_MANIFEST in namespace $NAMESPACE..."
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST"
;;
stop)
ensure_tools
if [[ ! -f "$CNPG_MANIFEST" ]]; then
echo "ERROR: CNPG manifest not found at $CNPG_MANIFEST" >&2
exit 1
fi
echo "Stopping CloudNative-PG cluster using $CNPG_MANIFEST ..."
kubectl delete -f "$CNPG_MANIFEST" --ignore-not-found
;;
status)
ensure_tools
echo "--- CloudNative-PG Cluster Status ($CNPG_CLUSTER_NAME) ---"
if kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" >/dev/null 2>&1; then
kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME"
echo ""
echo "CNPG Plugin Status:"
if kubectl cnpg version >/dev/null 2>&1; then
kubectl cnpg status "$CNPG_CLUSTER_NAME" -n "$NAMESPACE"
else
echo "Note: 'kubectl cnpg' plugin not found; skipping detailed status."
fi
else
echo "Cluster '$CNPG_CLUSTER_NAME' not found in namespace '$NAMESPACE'."
fi
;;
restart)
ensure_tools
"$0" stop
"$0" start
;;
initialize)
initialize
;;
update|reload)
update_reload
;;
*)
echo "Usage: $0 {create|delete|recreate|start|stop|status|restart|initialize|update|reload} [dbname]" >&2
exit 2
;;
esac

View File

@ -1,203 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# etc/init_k8s.sh
# Purpose: Manage k3d/k8s environment
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Preserve positional args while sourcing env
__PROLE_SAVED_ARGS=("$@")
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$HOME/.prole/env.sh"
fi
set -- "${__PROLE_SAVED_ARGS[@]}"
unset __PROLE_SAVED_ARGS
# Default values
VERBOSE=false
ENVIRONMENT="prod"
HOST=""
CLUSTER_NAME_DEFAULT="prole-dev-cluster"
CLUSTER_PORT_DEFAULT="6443"
usage() {
cat <<EOF
Usage: $0 [initialize|start|stop|restart|status] [options]
Actions:
initialize Create a k3d cluster (dev) or fetch kubeconfig (service/prod)
start Start the k3d cluster
stop Stop the k3d cluster
restart Restart the k3d cluster
status Show status of Docker and k3d cluster
Options:
-v, --verbose Enable verbose output
-e, --environment dev|service|prod (default: $ENVIRONMENT)
-h, --host Remote host for service/prod environments
-n, --name k3d cluster name
EOF
exit 1
}
log() {
echo "[INFO] $*"
}
debug() {
if [[ "$VERBOSE" == "true" ]]; then
echo "[DEBUG] $*"
fi
}
# Parse options
POSITIONAL_ARGS=()
CLUSTER_NAME=""
while [[ $# -gt 0 ]]; do
case $1 in
-v|--verbose)
VERBOSE=true
shift
;;
-e|--environment)
ENVIRONMENT="$2"
shift 2
;;
-h|--host)
HOST="$2"
shift 2
;;
-n|--name)
CLUSTER_NAME="$2"
shift 2
;;
*)
POSITIONAL_ARGS+=("$1")
shift
;;
esac
done
set -- "${POSITIONAL_ARGS[@]}"
ACTION=${1:-}
if [[ -z "$ACTION" ]]; then
usage
fi
ensure_tools() {
for t in k3d docker; do
command -v "$t" >/dev/null || { echo "ERROR: Missing required tool: $t" >&2; exit 1; }
done
}
ensure_docker() {
if ! docker info >/dev/null 2>&1; then
echo "ERROR: Docker is not running." >&2
exit 1
fi
}
initialize() {
ensure_tools
ensure_docker
if [[ "$ENVIRONMENT" == "dev" ]]; then
if [[ -z "$CLUSTER_NAME" ]]; then
if [[ -t 0 ]]; then
read -p "Enter k3d cluster name [$CLUSTER_NAME_DEFAULT]: " CLUSTER_NAME
CLUSTER_NAME=${CLUSTER_NAME:-$CLUSTER_NAME_DEFAULT}
else
CLUSTER_NAME=$CLUSTER_NAME_DEFAULT
fi
fi
if k3d cluster list "$CLUSTER_NAME" >/dev/null 2>&1; then
log "Cluster '$CLUSTER_NAME' already exists."
else
log "Creating k3d cluster '$CLUSTER_NAME'..."
k3d cluster create "$CLUSTER_NAME" -p "0.0.0.0:$CLUSTER_PORT_DEFAULT:6443@server:0"
fi
else
if [[ -n "$HOST" ]]; then
log "Environment is $ENVIRONMENT. Host is $HOST."
log "TODO: Fetch kubeconfig from $HOST (placeholder)."
else
log "Environment is $ENVIRONMENT. Use -h|--host to specify the remote cluster host."
fi
fi
}
status() {
ensure_tools
log "Checking Docker status..."
if docker info >/dev/null 2>&1; then
echo "Docker is running."
else
echo "Docker is NOT running."
fi
log "Listing k3d clusters..."
k3d cluster list
}
get_cluster_name() {
# If a name was provided or exists in env, use it.
# Otherwise, if there is only one cluster, use it.
# Finally, use default.
if [[ -n "${CLUSTER_NAME:-}" ]]; then
echo "$CLUSTER_NAME"
return
fi
local clusters
clusters=$(k3d cluster list --no-headers | awk '{print $1}')
local count
count=$(echo "$clusters" | grep -c . || true)
if [[ "$count" -eq 1 ]]; then
echo "$clusters"
else
echo "$CLUSTER_NAME_DEFAULT"
fi
}
case "$ACTION" in
initialize)
initialize
;;
status)
status
;;
start)
ensure_tools
CLUSTER_NAME=$(get_cluster_name)
log "Starting k3d cluster '$CLUSTER_NAME'..."
k3d cluster start "$CLUSTER_NAME"
;;
stop)
ensure_tools
CLUSTER_NAME=$(get_cluster_name)
log "Stopping k3d cluster '$CLUSTER_NAME'..."
k3d cluster stop "$CLUSTER_NAME"
;;
restart)
ensure_tools
CLUSTER_NAME=$(get_cluster_name)
log "Restarting k3d cluster '$CLUSTER_NAME'..."
k3d cluster stop "$CLUSTER_NAME"
k3d cluster start "$CLUSTER_NAME"
;;
*)
usage
;;
esac

View File

@ -1,490 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# init_openbao.sh
# Purpose:
# - Deploy OpenBao to Kubernetes (dev mode) and store admin ed25519 key pair
# Generate and apply a Kerberos krb5.conf ConfigMap for an external realm
# - Local Docker helpers for OpenBao (optional)
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Preserve positional args while sourcing env
__PROLE_SAVED_ARGS=("$@")
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$HOME/.prole/env.sh"
fi
set -- "${__PROLE_SAVED_ARGS[@]}"
unset __PROLE_SAVED_ARGS
if [[ -z "${PROLE_SERVICE:-}" ]]; then
echo "ERROR: PROLE_SERVICE is not defined in env. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
exit 1
fi
ACTION=${1:-}
# Defaults
NAMESPACE=${NAMESPACE:-prole}
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
OPENBAO_MANIFEST_DIR="$SCRIPT_DIR/../k8s/openbao"
# Kerberos realm defaults
KRB5_REALM=${KRB5_REALM:-PROLE.ORG}
DOMAIN=${DOMAIN:-$(echo "$KRB5_REALM" | tr 'A-Z' 'a-z')}
KRB5_KDC=${KRB5_KDC:-kdc.$DOMAIN}
KRB5_ADMIN=${KRB5_ADMIN:-}
# Secrets and token locations
SECRETS_DIR="$PROLE_SERVICE/secrets"
mkdir -p "$SECRETS_DIR"
admin_key_priv="$SECRETS_DIR/admin_ed25519.key"
admin_key_pub="$SECRETS_DIR/admin_ed25519.pub"
root_token_file="$SECRETS_DIR/openbao-root-token"
ensure_tools() {
for t in kubectl curl openssl base64; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
ensure_docker() {
command -v docker >/dev/null || { echo "Missing required tool: docker" >&2; exit 1; }
}
ensure_admin_keypair() {
# Ensure admin ed25519 keypair exists and a root token is available
mkdir -p "$SECRETS_DIR"
if [[ ! -f "$admin_key_priv" || ! -f "$admin_key_pub" ]]; then
echo "Generating admin ed25519 keypair in $SECRETS_DIR ..."
openssl genpkey -algorithm ED25519 -out "$admin_key_priv"
openssl pkey -in "$admin_key_priv" -pubout -out "$admin_key_pub"
chmod 0600 "$admin_key_priv"
fi
# Ensure a root token exists for OpenBao dev server interactions
if [[ ! -f "$root_token_file" || ! -s "$root_token_file" ]]; then
openssl rand -hex 24 >"$root_token_file"
chmod 0600 "$root_token_file"
fi
}
generate_openbao_manifests() {
mkdir -p "$OPENBAO_MANIFEST_DIR"
# Only create a simple deployment if none exists; otherwise respect current file
local deploy="$OPENBAO_MANIFEST_DIR/deployment.yaml"
if [[ ! -f "$deploy" ]]; then
cat >"$deploy" <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: $OPENBAO_NAME
namespace: $NAMESPACE
spec:
replicas: 1
selector:
matchLabels:
app: $OPENBAO_NAME
template:
metadata:
labels:
app: $OPENBAO_NAME
spec:
containers:
- name: $OPENBAO_NAME
image: $OPENBAO_IMAGE
args: ["server","-dev","-dev-listen-address=0.0.0.0:8200"]
ports:
- containerPort: 8200
---
apiVersion: v1
kind: Service
metadata:
name: $OPENBAO_NAME
namespace: $NAMESPACE
spec:
selector:
app: $OPENBAO_NAME
ports:
- name: http
port: 8200
targetPort: 8200
EOF
fi
}
generate_kerberos_configmap() {
mkdir -p "$OPENBAO_MANIFEST_DIR"
local domain_lower
domain_lower=$(echo "$DOMAIN" | tr 'A-Z' 'a-z')
# Split KRB5_KDC on comma into bash array
local IFS=','
read -r -a __kdcs <<< "$KRB5_KDC"
local kdc_lines=""
local kdc
for kdc in "${__kdcs[@]}"; do
kdc_lines+=$' kdc = '"$kdc"$'\n'
done
local admin_server admin_line
admin_server=${KRB5_ADMIN:-"${__kdcs[0]:-}"}
admin_line=""
if [[ -n "$admin_server" ]]; then
# 8 spaces to remain within the YAML literal block indentation
admin_line=$' admin_server = '"$admin_server"$'\n'
fi
cat >"$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml" <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: prole-krb5-conf
namespace: $NAMESPACE
data:
krb5.conf: |
[libdefaults]
default_realm = $KRB5_REALM
dns_lookup_realm = true
dns_lookup_kdc = true
[realms]
$KRB5_REALM = {
${kdc_lines}${admin_line} }
[domain_realm]
.$domain_lower = $KRB5_REALM
$domain_lower = $KRB5_REALM
EOF
}
apply_k8s() {
echo "Applying OpenBao manifest to namespace '$NAMESPACE' ..."
if [[ -f "$SCRIPT_DIR/../k8s/prole/openbao-statefulset.yaml" ]]; then
kubectl apply -n "$NAMESPACE" -f "$SCRIPT_DIR/../k8s/prole/openbao-statefulset.yaml"
kubectl apply -n "$NAMESPACE" -f "$SCRIPT_DIR/../k8s/prole/openbao-service.yaml"
else
kubectl apply -n "$NAMESPACE" -f "$OPENBAO_MANIFEST_DIR/deployment.yaml"
fi
echo "Applying Kerberos ConfigMap (external realm) to namespace '$NAMESPACE' ..."
kubectl apply -n "$NAMESPACE" -f "$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml"
}
wait_for_openbao() {
echo "Waiting for OpenBao to become ready ..."
if kubectl get statefulset/$OPENBAO_NAME -n "$NAMESPACE" >/dev/null 2>&1; then
kubectl rollout status statefulset/$OPENBAO_NAME -n "$NAMESPACE" --timeout=120s
else
kubectl rollout status deploy/$OPENBAO_NAME -n "$NAMESPACE" --timeout=120s
fi
}
init_openbao_kv_and_store_admin_key() {
local token
token=$(cat "$root_token_file")
# Enable kv (if not already) and write keys
local svc="http://127.0.0.1:18200"
echo "Configuring KV at $svc ..."
# Check if kv/ is already mounted
if curl -sS -H "X-Vault-Token: $token" "$svc/v1/sys/mounts" | grep -q '"kv/":'; then
echo "KV path 'kv/' is already enabled."
else
curl -sS -H "X-Vault-Token: $token" -X POST "$svc/v1/sys/mounts/kv" \
-d '{"type":"kv","options":{"version":"2"}}' >/dev/null 2>&1 || true
fi
local priv pub
priv=$(base64 <"$admin_key_priv" | tr -d '\n')
pub=$(base64 <"$admin_key_pub" | tr -d '\n')
# Check if admin key already exists and matches
local existing_data
existing_data=$(curl -sS -H "X-Vault-Token: $token" "$svc/v1/kv/data/prole/admin" 2>/dev/null || true)
if [[ -n "$existing_data" ]]; then
local ex_priv ex_pub
ex_priv=$(echo "$existing_data" | grep -o '"admin_private_key_b64":"[^"]*' | cut -d'"' -f4 || true)
ex_pub=$(echo "$existing_data" | grep -o '"admin_public_key_b64":"[^"]*' | cut -d'"' -f4 || true)
if [[ "$ex_priv" == "$priv" && "$ex_pub" == "$pub" ]]; then
echo "Admin key pair in OpenBao kv/prole/admin is already up to date."
else
echo "Writing admin key pair to kv/prole/admin ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/admin" \
-d "{\"data\":{\"admin_private_key_b64\":\"$priv\",\"admin_public_key_b64\":\"$pub\"}}" >/dev/null
echo "Stored admin key pair in OpenBao kv/prole/admin."
fi
else
echo "Writing admin key pair to kv/prole/admin ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/admin" \
-d "{\"data\":{\"admin_private_key_b64\":\"$priv\",\"admin_public_key_b64\":\"$pub\"}}" >/dev/null
echo "Stored admin key pair in OpenBao kv/prole/admin."
fi
# Store User SSH keys if they exist
local user_key_priv="$HOME/.ssh/id_prole_ed25519"
local user_key_pub="$HOME/.ssh/id_prole_ed25519.pub"
if [[ -f "$user_key_priv" && -f "$user_key_pub" ]]; then
local u_priv u_pub
u_priv=$(base64 <"$user_key_priv" | tr -d '\n')
u_pub=$(base64 <"$user_key_pub" | tr -d '\n')
local username=${PROLE_DB_USER:-"prole"}
echo "Writing user SSH keys for '$username' to kv/prole/user ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/user" \
-d "{\"data\":{\"username\":\"$username\",\"private_key_b64\":\"$u_priv\",\"public_key_b64\":\"$u_pub\"}}" >/dev/null
echo "Stored user SSH keys in OpenBao kv/prole/user."
fi
if [[ -n "$db_pass" ]]; then
local username=${PROLE_DB_USER:-"prole"}
echo "Writing database user password for '$username' to kv/prole/db ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/db" \
-d "{\"data\":{\"username\":\"$username\",\"password\":\"$db_pass\"}}" >/dev/null
echo "Stored database password in OpenBao kv/prole/db."
fi
}
# Removed: prompt_admin_password_and_apply_secret (no AD admin secret required)
docker_start() {
echo "Starting local Docker container for OpenBao ..."
ensure_docker
docker rm -f "$OPENBAO_NAME" >/dev/null 2>&1 || true
# OpenBao dev
local token
token=$(cat "$root_token_file" 2>/dev/null || true)
if [[ -z "$token" ]]; then
token=$(openssl rand -hex 24)
printf "%s" "$token" >"$root_token_file"
chmod 0600 "$root_token_file"
fi
docker run -d --name "$OPENBAO_NAME" -p 18200:8200 \
"$OPENBAO_IMAGE" server -dev -dev-listen-address=0.0.0.0:8200 -dev-root-token-id="$token"
echo "Docker container started: $OPENBAO_NAME"
}
docker_stop() {
docker rm -f "$OPENBAO_NAME" >/dev/null 2>&1 || true
echo "Stopped OpenBao container if it was running."
}
docker_restart() {
docker_stop
docker_start
}
# status command implementation wrapped in a function to avoid top-level 'local'
cmd_status() {
echo "--- init_primary_domain status ---"
echo "Namespace: $NAMESPACE"
echo "OpenBao name: $OPENBAO_NAME"
echo "Manifest dir: $OPENBAO_MANIFEST_DIR"
# Files/manifests present
local ok=0
if [[ -f "$OPENBAO_MANIFEST_DIR/deployment.yaml" ]]; then
echo "[OK] OpenBao deployment manifest exists"
else
echo "[MISSING] $OPENBAO_MANIFEST_DIR/deployment.yaml"
ok=1
fi
if [[ -f "$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml" ]]; then
echo "[OK] Kerberos ConfigMap manifest exists"
else
echo "[MISSING] $OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml"
ok=1
fi
# K8s resources
if kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" >/dev/null 2>&1; then
local ready desired
ready=$(kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0")
desired=$(kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" -o jsonpath='{.status.replicas}' 2>/dev/null || echo "0")
ready=${ready:-0}
desired=${desired:-0}
if [[ "$ready" == "$desired" && "$ready" != "0" ]]; then
echo "[OK] OpenBao StatefulSet running ($ready/$desired ready)"
else
echo "[WARN] OpenBao StatefulSet not fully ready ($ready/$desired)"
ok=1
fi
elif kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" >/dev/null 2>&1; then
# Get readiness
local ready desired
ready=$(kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0")
desired=$(kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" -o jsonpath='{.status.replicas}' 2>/dev/null || echo "0")
ready=${ready:-0}
desired=${desired:-0}
if [[ "$ready" == "$desired" && "$ready" != "0" ]]; then
echo "[OK] OpenBao Deployment running ($ready/$desired ready)"
else
echo "[WARN] OpenBao Deployment not fully ready ($ready/$desired)"
ok=1
fi
else
echo "[MISSING] OpenBao Deployment or StatefulSet '$OPENBAO_NAME' in namespace '$NAMESPACE'"
ok=1
fi
if kubectl -n "$NAMESPACE" get configmap prole-krb5-conf >/dev/null 2>&1; then
echo "[OK] Kerberos ConfigMap 'prole-krb5-conf' present"
else
echo "[MISSING] Kerberos ConfigMap 'prole-krb5-conf' in namespace '$NAMESPACE'"
ok=1
fi
# Docker (optional local mode)
if command -v docker >/dev/null 2>&1; then
if docker ps --format '{{.Names}}' | grep -Fxq "$OPENBAO_NAME"; then
echo "[OK] Docker container '$OPENBAO_NAME' is running"
else
# Not necessarily an error; we primarily use k8s
echo "[INFO] Docker container '$OPENBAO_NAME' not running (k8s mode may be in use)"
fi
fi
# Secrets and tokens
if [[ -f "$admin_key_priv" && -f "$admin_key_pub" ]]; then
echo "[OK] Admin ed25519 keypair present in $SECRETS_DIR"
else
echo "[MISSING] Admin keypair files in $SECRETS_DIR"
ok=1
fi
if [[ -s "$root_token_file" ]]; then
echo "[OK] OpenBao root token file present"
else
echo "[MISSING] OpenBao root token file ($root_token_file)"
ok=1
fi
# OpenBao reachability and token validity (best-effort)
# Prefer explicit override, then localhost port-forward, then cluster DNS
local svc_url
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
svc_url="$PROLE_OPENBAO_URL"
elif curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then
svc_url="http://127.0.0.1:18200"
else
svc_url="http://$OPENBAO_NAME.$NAMESPACE.svc.cluster.local:8200"
fi
if curl -sS "$svc_url/v1/sys/health" >/dev/null 2>&1; then
echo "[OK] OpenBao service reachable"
if [[ -s "$root_token_file" ]]; then
local code
code=$(curl -s -o /dev/null -w "%{http_code}" -H "X-Vault-Token: $(cat "$root_token_file")" "$svc_url/v1/sys/mounts" || echo "000")
if [[ "$code" == "200" ]]; then
echo "[OK] OpenBao root token authenticates"
else
echo "[WARN] OpenBao root token did not authenticate (HTTP $code)"
ok=1
fi
fi
else
echo "[WARN] OpenBao service not reachable at $svc_url"
ok=1
fi
# Service certificate existence (CNPG TLS)
local CNPG_CLUSTER_NAME
CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db}
if kubectl -n "$NAMESPACE" get secret "${CNPG_CLUSTER_NAME}-tls" >/dev/null 2>&1; then
echo "[OK] Service certificate secret '${CNPG_CLUSTER_NAME}-tls' exists"
else
echo "[INFO] Service certificate secret '${CNPG_CLUSTER_NAME}-tls' not found"
fi
echo "-------------------------------------"
if [[ $ok -eq 0 ]]; then
echo "Status: OK"
return 0
else
echo "Status: Issues detected"
return 1
fi
}
case "$ACTION" in
start)
ensure_tools
ensure_docker
ensure_admin_keypair
docker_start
;;
status)
ensure_tools
cmd_status
;;
stop)
ensure_docker
docker_stop
;;
restart)
ensure_tools
ensure_docker
ensure_admin_keypair
docker_restart
;;
initialize)
ensure_tools
# Read password from stdin if provided
db_pass=""
if [[ ! -t 0 ]]; then
read -r db_pass
fi
ensure_admin_keypair
generate_openbao_manifests
generate_kerberos_configmap
apply_k8s
if [[ -n "$db_pass" ]]; then
local username=${PROLE_DB_USER:-"prole"}
echo "Creating database user secret 'prole-db-user' for '$username' ..."
kubectl create secret generic prole-db-user -n "$NAMESPACE" \
--from-literal=username="$username" \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -n "$NAMESPACE" -f -
echo "Creating database superuser secret 'prole-db-superuser' ..."
kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \
--from-literal=username=postgres \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -n "$NAMESPACE" -f -
fi
wait_for_openbao
# Ensure port-forward is running for OpenBao
echo "Ensuring port-forward for OpenBao is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart openbao
if ! curl -sS http://127.0.0.1:18200/v1/sys/health >/dev/null 2>&1; then
echo "ERROR: OpenBao not reachable at http://127.0.0.1:18200." >&2
echo "Please start port-forwards: $SCRIPT_DIR/init_port_forwards.sh start openbao" >&2
exit 1
fi
init_openbao_kv_and_store_admin_key
echo "Initialization complete. Manifests in: $OPENBAO_MANIFEST_DIR"
;;
update|reload)
generate_openbao_manifests
generate_kerberos_configmap
apply_k8s
echo "Re-applied manifests."
;;
*)
echo "Usage: $0 {start|stop|status|restart|initialize|update|reload}" >&2
exit 2
;;
esac

View File

@ -1,537 +0,0 @@
#!/usr/bin/env bash
set -u
# init_port_forwards.sh
# Portable-ish (macOS, Ubuntu, Raspberry Pi OS, Alpine) bash init-style script
# Manages kubectl port-forward daemons defined in an XML file.
PROG="init_port_forwards"
PROLE_HOME="${PROLE_HOME:-/Users/chrisfu/dev/prole}"
VERBOSE=0
CONFIG_FILE="$PROLE_HOME/conf/port-mappings.properties"
usage() {
cat <<EOF
Usage:
$PROG [-v|--verbose] [-c|--config-file=FILE] <start|stop|restart|status> [component]
Options:
-c, --config-file=FILE Path to local-ports.properties (XML)
-v, --verbose Verbose output
Examples:
$PROG -c ./port-mappings.properties start
$PROG stop openbao
$PROG --verbose status
EOF
}
TARGET_ID=""
log() { printf '%s\n' "$*"; }
vlog() { [ "$VERBOSE" -eq 1 ] && printf '[verbose] %s\n' "$*" || true; }
err() { printf '[error] %s\n' "$*" >&2; }
have() { command -v "$1" >/dev/null 2>&1; }
# Choose a writable state dir for pid/log files:
# - Prefer XDG_RUNTIME_DIR if set and writable
# - Else /var/run if writable (rare without root)
# - Else ~/.local/state
# - Else /tmp
state_dir() {
if [ -n "${XDG_RUNTIME_DIR:-}" ] && [ -w "${XDG_RUNTIME_DIR:-}" ]; then
printf '%s/%s' "$XDG_RUNTIME_DIR" "$PROG"
return
fi
if [ -d "/var/run" ] && [ -w "/var/run" ]; then
printf '/var/run/%s' "$PROG"
return
fi
if [ -n "${HOME:-}" ]; then
mkdir -p "$HOME/.local/state" >/dev/null 2>&1 || true
if [ -d "$HOME/.local/state" ] && [ -w "$HOME/.local/state" ]; then
printf '%s/.local/state/%s' "$HOME" "$PROG"
return
fi
fi
printf '/tmp/%s' "$PROG"
}
STATE_DIR="$(state_dir)"
PID_DIR="$STATE_DIR/pids"
LOG_DIR="$STATE_DIR/logs"
ensure_dirs() {
mkdir -p "$PID_DIR" "$LOG_DIR" 2>/dev/null || true
if [ ! -d "$PID_DIR" ] || [ ! -d "$LOG_DIR" ]; then
err "Unable to create state directories under: $STATE_DIR"
exit 1
fi
}
# Use kubectl for actions; kubecolor is used only for prettier status output.
KUBECTL="kubectl"
detect_kubectl() {
if have kubectl; then
KUBECTL="kubectl"
else
err "kubectl not found in PATH"
exit 1
fi
}
# Basic environment validation
validate_env() {
detect_kubectl
ensure_dirs
if ! "$KUBECTL" version --client >/dev/null 2>&1; then
err "kubectl seems broken or not executable"
exit 1
fi
# Can we reach the cluster?
if ! "$KUBECTL" cluster-info >/dev/null 2>&1; then
err "kubectl cannot reach a cluster (check KUBECONFIG/context)"
err "Try: kubectl config get-contexts && kubectl config use-context <ctx>"
exit 1
fi
}
# ---- Preflight: Docker + k3d awareness ----
# Return 0 when Docker CLI can talk to a running daemon.
docker_is_running() {
if ! have docker; then
return 1
fi
docker info >/dev/null 2>&1
}
# Hard-fail with clean guidance when Docker is not up (used for start/restart).
ensure_docker_running() {
if docker_is_running; then
return 0
fi
if ! have docker; then
err "Docker CLI not found. Please install Docker Desktop or docker CLI."
else
err "Docker is not running. Start Docker Desktop and wait until it is ready."
fi
err "Tip (macOS): open -a Docker"
err "Then re-run: $PROG start"
exit 3
}
# Detect k3d cluster name from env or current kubectl context.
# - If K3D_CLUSTER is set, use it.
# - Else, if current-context starts with 'k3d-', strip prefix to get name.
detect_k3d_context() {
if [ -n "${K3D_CLUSTER:-}" ]; then
printf '%s' "$K3D_CLUSTER"
return 0
fi
local ctx
ctx="$($KUBECTL config current-context 2>/dev/null || echo)"
case "$ctx" in
k3d-*) printf '%s' "${ctx#k3d-}"; return 0 ;;
*) return 1 ;;
esac
}
# Return 0 if the given k3d cluster exists and is running.
k3d_cluster_is_running() {
local name="$1"
have k3d || return 1
# Use json output when available; fall back to grep otherwise
if k3d cluster list -o json >/dev/null 2>&1; then
k3d cluster list -o json 2>/dev/null | grep -q '"name"\s*:\s*"'"$name"'"' && \
k3d cluster list -o json 2>/dev/null | sed -n 's/.*"name"\s*:\s*"\([^"]\+\)".*"serversRunning"\s*:\s*\([0-9]\+\).*/\1 \2/p' | awk -v n="$name" '$1==n {exit ($2>0)?0:1}'
return $?
else
k3d cluster list 2>/dev/null | grep -E "^$name\s" | grep -q running
return $?
fi
}
# If current context indicates k3d, ensure the cluster is up; otherwise no-op.
ensure_k3d_ready_if_applicable() {
local k3d_name
if ! k3d_name="$(detect_k3d_context)"; then
return 0
fi
if ! have k3d; then
err "k3d is not installed but kubectl context suggests k3d (context=$("$KUBECTL" config current-context))."
err "Install k3d: brew install k3d (macOS)"
err "Or switch context: kubectl config use-context <non-k3d-context>"
exit 4
fi
if ! k3d_cluster_is_running "$k3d_name"; then
err "k3d cluster '$k3d_name' is not running."
err "Start it: k3d cluster start $k3d_name"
err "Then re-run: $PROG start"
exit 4
fi
}
pid_file_for() { printf '%s/%s.pid' "$PID_DIR" "$1"; }
log_file_for() { printf '%s/%s.log' "$LOG_DIR" "$1"; }
is_pid_running() {
# Return 0 if pid exists and running, else 1
# kill -0 is portable.
local pid="$1"
[ -n "$pid" ] && kill -0 "$pid" >/dev/null 2>&1
}
read_pid() {
local pf="$1"
[ -f "$pf" ] || return 1
# shellcheck disable=SC2162
read pid <"$pf" || return 1
printf '%s' "$pid"
}
write_pid() {
local pf="$1" pid="$2"
printf '%s\n' "$pid" >"$pf"
}
remove_pidfile() {
local pf="$1"
rm -f "$pf" >/dev/null 2>&1 || true
}
# ---- XML parsing (simple attribute extraction) ----
# We parse lines containing: <mapping ... />
# Attributes must use double quotes in the XML (as in the sample).
get_attr() {
# $1 = line, $2 = attribute name
# outputs value or empty
printf '%s\n' "$1" | sed -n "s/.*$2=\"\([^\"]*\)\".*/\1/p"
}
foreach_mapping() {
# Calls a provided function with mapping fields:
# callback id ns target address hostPort servicePort protocol description
local callback="$1"
[ -f "$CONFIG_FILE" ] || { err "Config file not found: $CONFIG_FILE"; exit 1; }
# Support both single-line and multi-line self-closing mapping tags, e.g.:
# <mapping id="x" ... /> OR lines spanning multiple lines until "/>".
local in_mapping=0 in_comment=0 buffer="" line
while IFS= read -r line; do
# Handle XML comments: skip anything between <!-- and -->
if [ $in_comment -eq 1 ]; then
case "$line" in
*"-->"*) in_comment=0; continue ;;
*) continue ;;
esac
fi
case "$line" in
*"<!--"*)
case "$line" in
*"-->"*)
# single-line comment; skip line
continue
;;
*)
in_comment=1
continue
;;
esac
;;
esac
if [ $in_mapping -eq 0 ]; then
case "$line" in
*"<mapping"*)
in_mapping=1
buffer="$line"
;;
*)
continue
;;
esac
else
# Accumulate lines until we see the closing '/>'
buffer="$buffer $line"
fi
if [ $in_mapping -eq 1 ] && printf '%s' "$line" | grep -q "/>"; then
# Normalize whitespace to make attribute extraction robust
local merged
merged=$(printf '%s\n' "$buffer" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g')
local id ns target address hostPort servicePort protocol description
id="$(get_attr "$merged" "id")"
ns="$(get_attr "$merged" "namespace")"
target="$(get_attr "$merged" "target")"
address="$(get_attr "$merged" "address")"
hostPort="$(get_attr "$merged" "hostPort")"
servicePort="$(get_attr "$merged" "servicePort")"
protocol="$(get_attr "$merged" "protocol")"
description="$(get_attr "$merged" "description")"
# Basic validation
if [ -z "$id" ] || [ -z "$ns" ] || [ -z "$target" ] || [ -z "$hostPort" ] || [ -z "$servicePort" ]; then
err "Invalid mapping (missing required attributes): $merged"
exit 1
fi
# Filter by TARGET_ID if set
if [ -n "$TARGET_ID" ] && [ "$id" != "$TARGET_ID" ]; then
in_mapping=0
buffer=""
continue
fi
if [ -z "$address" ]; then address="127.0.0.1"; fi
if [ -z "$protocol" ]; then protocol="TCP"; fi
vlog "mapping: id=$id ns=$ns target=$target address=$address hostPort=$hostPort servicePort=$servicePort protocol=$protocol"
"$callback" "$id" "$ns" "$target" "$address" "$hostPort" "$servicePort" "$protocol" "$description"
# Reset accumulator
in_mapping=0
buffer=""
fi
done <"$CONFIG_FILE"
}
build_port_forward_cmd() {
# echo a command string
local ns="$1" target="$2" address="$3" hostPort="$4" servicePort="$5"
# kubectl port-forward -n <ns> --address <addr> <target> <local>:<remote>
printf '%s port-forward -n %s --address %s %s %s:%s' \
"$KUBECTL" "$ns" "$address" "$target" "$hostPort" "$servicePort"
}
start_port_forward() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
local pidfile logfile cmd pid
# Aggressively stop existing processes before starting to avoid port conflicts
stop_port_forward "$id" "$ns" "$target" "$address" "$hostPort" "$servicePort" "$protocol" "$description"
pidfile="$(pid_file_for "$id")"
logfile="$(log_file_for "$id")"
cmd="$(build_port_forward_cmd "$ns" "$target" "$address" "$hostPort" "$servicePort")"
log "Starting: $id $address:$hostPort -> $target:$servicePort ($ns) ${description:-}"
vlog "Command: $cmd"
# Start in background, keep output in log.
# nohup is available on macOS/Linux; redirect stdin from /dev/null to detach.
nohup sh -c "$cmd" >>"$logfile" 2>&1 </dev/null &
pid="$!"
write_pid "$pidfile" "$pid"
# Quick verification
sleep 0.2
if is_pid_running "$pid"; then
vlog "$id started (pid=$pid, log=$logfile)"
return 0
else
err "$id failed to start (see log: $logfile)"
remove_pidfile "$pidfile"
return 1
fi
}
stop_port_forward() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
local pidfile pid
pidfile="$(pid_file_for "$id")"
pid=""
if [ -f "$pidfile" ]; then
pid="$(read_pid "$pidfile" || true)"
fi
if [ -n "${pid:-}" ] && is_pid_running "$pid"; then
log "Stopping: $id (pid=$pid)"
kill "$pid" >/dev/null 2>&1 || true
# wait a moment, then SIGKILL if needed
local i
for i in 1 2 3 4 5; do
if ! is_pid_running "$pid"; then break; fi
sleep 0.2
done
if is_pid_running "$pid"; then
err "$id did not stop gracefully; sending SIGKILL"
kill -9 "$pid" >/dev/null 2>&1 || true
fi
else
if [ -f "$pidfile" ]; then
vlog "$id stale pidfile (pid=$pid not running)"
fi
fi
remove_pidfile "$pidfile"
# Aggressively remove any other matching kubectl port-forward processes
# Search for processes that match: kubectl port-forward -n <ns> ... <target> <hostPort>:<servicePort>
local extra_pids
extra_pids=$(ps -ef | grep "port-forward" | grep "\-n" | grep "$ns" | grep "$target" | grep "$hostPort:$servicePort" | grep -v grep | awk '{print $2}')
for epid in $extra_pids; do
if [ "$epid" != "$pid" ]; then
log "Cleaning up orphan process for $id (pid=$epid)"
kill -9 "$epid" >/dev/null 2>&1 || true
fi
done
}
status_one() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
local pidfile pid
pidfile="$(pid_file_for "$id")"
pid=""
if [ -f "$pidfile" ]; then
pid="$(read_pid "$pidfile" || true)"
fi
if [ -n "${pid:-}" ] && is_pid_running "$pid"; then
printf 'RUNNING %-12s pid=%-7s %s:%s -> %s:%s ns=%s %s\n' \
"$id" "$pid" "$address" "$hostPort" "$target" "$servicePort" "$ns" "${description:-}"
# ps details (portable flags vary; use a conservative format)
ps -p "$pid" -o pid=,ppid=,etime=,command= 2>/dev/null | sed 's/^/ /' || true
else
printf 'STOPPED %-12s (no live pid) %s:%s -> %s:%s ns=%s %s\n' \
"$id" "$address" "$hostPort" "$target" "$servicePort" "$ns" "${description:-}"
fi
}
do_start() {
validate_env
# Preflight: require Docker daemon and k3d (if applicable)
ensure_docker_running
ensure_k3d_ready_if_applicable
foreach_mapping start_port_forward
}
do_stop() {
# stop doesn't require cluster access, but it does need state dirs
ensure_dirs
foreach_mapping stop_port_forward
}
do_restart() {
validate_env
# Preflight: require Docker daemon and k3d (if applicable)
ensure_docker_running
ensure_k3d_ready_if_applicable
foreach_mapping stop_port_forward
foreach_mapping start_port_forward
}
do_status() {
validate_env
# Non-fatal awareness messages
if docker_is_running; then
log "[OK] Docker daemon is running"
else
if have docker; then
log "[WARN] Docker is not running"
else
log "[WARN] Docker CLI not found"
fi
fi
local _k3d_name
if _k3d_name="$(detect_k3d_context)"; then
if have k3d; then
if k3d_cluster_is_running "$_k3d_name"; then
log "[OK] k3d cluster '$_k3d_name' is running"
else
log "[WARN] k3d cluster '$_k3d_name' is not running"
fi
else
log "[WARN] k3d not installed but context suggests k3d (cluster='$_k3d_name')"
fi
fi
log "== Context / Cluster =="
"$KUBECTL" config current-context 2>/dev/null | sed 's/^/ context: /' || true
"$KUBECTL" cluster-info 2>/dev/null | sed 's/^/ /' || true
log ""
log "== Port-forward processes =="
foreach_mapping status_one
log ""
log "== Quick k3d awareness checks (best-effort) =="
# If user is on k3d, current-context often includes k3d-... but not guaranteed.
# Show nodes + a few namespaces/services related to mappings (best effort).
"$KUBECTL" get nodes -o wide 2>/dev/null | sed 's/^/ /' || true
log ""
"$KUBECTL" get ns 2>/dev/null | sed 's/^/ /' || true
log ""
# For each mapping, try to show target existence
log "== Target existence (best-effort) =="
_target_check() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
# kubectl get <target> -n <ns>
# If target is like "svc/name", "deploy/name", etc.
if "$KUBECTL" get -n "$ns" "$target" >/dev/null 2>&1; then
printf 'OK %-12s %s (ns=%s)\n' "$id" "$target" "$ns"
else
printf 'MISSING %-12s %s (ns=%s)\n' "$id" "$target" "$ns"
fi
}
foreach_mapping _target_check
}
# ---- arg parsing ----
ACTION=""
while [ $# -gt 0 ]; do
case "$1" in
start|stop|restart|status)
if [ -z "$ACTION" ]; then
ACTION="$1"
else
TARGET_ID="$1"
fi
shift
;;
-v|--verbose)
VERBOSE=1
shift
;;
-c)
shift
[ $# -gt 0 ] || { err "-c requires a file path"; usage; exit 2; }
CONFIG_FILE="$1"
shift
;;
--config-file=*)
CONFIG_FILE="${1#*=}"
shift
;;
-h|--help)
usage
exit 0
;;
*)
if [ -z "$ACTION" ]; then
err "Unknown arg: $1"
usage
exit 2
fi
TARGET_ID="$1"
shift
;;
esac
done
[ -n "$ACTION" ] || { usage; exit 2; }
case "$ACTION" in
start) do_start ;;
stop) do_stop ;;
restart) do_restart ;;
status) do_status ;;
esac

View File

@ -1,215 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# init_prole-db.sh
# Purpose:
# - Manage prole-db CloudNative-PG cluster operations (deploy, start, stop, etc.)
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ACTION=${1:-}
VERSION=${2:-latest}
# Load env
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
source "$HOME/.prole/env.sh"
fi
CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db}
NAMESPACE=${NAMESPACE:-prole}
CNPG_MANIFEST="$SCRIPT_DIR/../k8s/prole/prole-db.yaml"
ensure_tools() {
for t in kubectl jq; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
get_latest_image() {
local version_file="$SCRIPT_DIR/../conf/postgresql/.version"
if [[ -f "$version_file" ]]; then
echo "prole-db:$(cat "$version_file" | tr -d '[:space:]')"
else
echo "prole-db:17.7-033"
fi
}
start() {
ensure_tools
echo "Checking dependencies..."
# 1. k3d is running
if ! command -v k3d >/dev/null || ! k3d cluster list >/dev/null 2>&1; then
echo "ERROR: k3d is not running or not installed." >&2
exit 1
fi
# 2. cnpg operator is loaded
if ! kubectl get deployment -n cnpg-system cnpg-controller-manager >/dev/null 2>&1; then
echo "ERROR: CloudNative-PG operator is not loaded." >&2
exit 1
fi
# 3. openbao is configured
if ! kubectl get statefulset openbao -n "$NAMESPACE" >/dev/null 2>&1 && ! kubectl get deployment openbao -n "$NAMESPACE" >/dev/null 2>&1; then
echo "ERROR: OpenBao is not deployed." >&2
exit 1
fi
# Compare latest image with deployed
local latest_image
latest_image=$(get_latest_image)
local current_image
current_image=$(kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.imageName}' 2>/dev/null || echo "")
if [[ "$current_image" != "$latest_image" ]]; then
echo "Updating cluster image from '$current_image' to '$latest_image'..."
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{\"spec\": {\"imageName\": \"$latest_image\"}}"
# Force a rollout to ensure the new image is pulled even if it was just a tag update (though we use unique tags)
kubectl cnpg restart "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" || true
else
echo "Cluster is already using the latest image: $latest_image"
fi
echo "Starting prole-db cluster (ensuring manifest is applied)..."
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST"
}
stop() {
ensure_tools
echo "Stopping prole-db cluster $CNPG_CLUSTER_NAME..."
# Identify instances
local instances
instances=$(kubectl get pods -n "$NAMESPACE" -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" -o jsonpath='{.items[*].metadata.name}')
if [[ -z "$instances" ]]; then
echo "No instances found for cluster $CNPG_CLUSTER_NAME."
return
fi
# Determine replicas and primary
local primary
primary=$(kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" -o jsonpath='{.status.currentPrimary}')
echo "Primary: $primary"
# Shutdown replicas first
for pod in $instances; do
if [[ "$pod" != "$primary" ]]; then
echo "Shutting down replica $pod..."
kubectl exec -n "$NAMESPACE" "$pod" -c postgres -- psql -U postgres -c "CHECKPOINT;" || true
fi
done
# Shutdown primary last
if [[ -n "$primary" ]]; then
echo "Shutting down primary $primary..."
kubectl exec -n "$NAMESPACE" "$primary" -c postgres -- psql -U postgres -c "CHECKPOINT;" || true
fi
echo "Deleting cluster resource..."
kubectl delete -f "$CNPG_MANIFEST" --ignore-not-found
}
restart() {
ensure_tools
echo "Restarting prole-db cluster..."
kubectl cnpg restart "$CNPG_CLUSTER_NAME" -n "$NAMESPACE"
}
deploy() {
ensure_tools
local image
if [[ "$VERSION" == "latest" ]]; then
image=$(get_latest_image)
else
image="prole-db:$VERSION"
fi
echo "Deploying $image to cluster $CNPG_CLUSTER_NAME..."
# If cluster doesn't exist, use init_cloudnative_pg.sh create first
if ! kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" >/dev/null 2>&1; then
echo "Cluster not found. Running init_cloudnative_pg.sh create..."
bash "$SCRIPT_DIR/init_cloudnative_pg.sh" create
fi
# Ensure image is updated if manifest has an older version
# First, apply the manifest to ensure the cluster exists/is updated
echo "Applying manifest $CNPG_MANIFEST..."
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST"
# Then force the specific image version via patch if different
local current_image
current_image=$(kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.imageName}' 2>/dev/null || echo "")
if [[ "$current_image" != "$image" ]]; then
echo "Patching cluster to use image '$image' (was '$current_image')..."
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{\"spec\": {\"imageName\": \"$image\"}}"
# Force a rollout
kubectl cnpg restart "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" || true
fi
}
rollback() {
ensure_tools
echo "Rollback: Updating to previous image (manually specified version or fallback)..."
if [[ "$VERSION" == "latest" ]]; then
echo "Please specify a version to rollback to. Usage: $0 rollback <version>"
exit 1
fi
deploy
}
backup() {
echo "Backup - tbd, when we configure s3 or other block store"
}
reset() {
ensure_tools
echo "Resetting prole-db cluster..."
echo "Removing cnpg instances and pods..."
kubectl delete cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --ignore-not-found
# Wait for deletion
kubectl wait --for=delete cluster/"$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --timeout=60s || true
echo "Reinstantiating..."
deploy
}
case "$ACTION" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
deploy)
deploy
;;
rollback)
rollback
;;
backup)
backup
;;
reset)
reset
;;
*)
echo "Usage: $0 {start|stop|restart|deploy|rollback|backup|reset} [version]" >&2
exit 2
;;
esac

View File

@ -1,29 +0,0 @@
#!/usr/bin/env bash
# Prole environment configuration
# This file is generated by the installer. Source it in new shells, or execute as a wrapper:
# "$PROLE_HOME/env.sh" <command> [args…]
# shellcheck shell=bash
export PROLE_HOME="<MagicMock name='mock.Entry().get().strip()' id='4491602512'>"
export PROLE_CONF="<MagicMock name='mock.Entry().get().strip()' id='4491602512'>"
export PROLE_DATA="<MagicMock name='mock.Entry().get().strip()' id='4491602512'>"
export PROLE_LOGS="<MagicMock name='mock.Entry().get().strip()' id='4491602512'>"
export PROLE_SERVICE="<MagicMock name='mock.Entry().get().strip()' id='4491602512'>"
# Ensure PATH works for GUI-launched shells (Docker, Ollama, etc.)
_prole_add_path() { case ":${PATH}:" in *":$1:"*) ;; *) PATH="$1:${PATH:-}" ;; esac; }
_prole_add_path "$PROLE_HOME/bin"
_prole_add_path "/opt/homebrew/bin"
_prole_add_path "/usr/local/bin"
_prole_add_path "/usr/bin"
_prole_add_path "/bin"
_prole_add_path "/usr/sbin"
_prole_add_path "/sbin"
export PATH
# Add custom paths below if needed (examples):
# _prole_add_path "/Applications/Ollama.app/Contents/MacOS"
# If executed with arguments, run them under this environment
if [ "$#" -gt 0 ]; then
exec "$@"
fi

View File

@ -1,15 +0,0 @@
myrddin.prole.org 10.0.0.3
raspberry.prole.org 10.0.0.4
pi.prole.org 10.0.0.5
synology.prole.org 10.0.0.203
morgoth.prole.org 10.0.0.204
zinfandel.prole.org 10.0.0.205
aventage.prole.org 10.0.0.206
retropie.prole.org 10.0.0.207
fairyland.prole.org 10.0.0.208
k8s.prole.org zinfandel.prole.org
mc.prole.org 73.15.20.166
morana.prole.org 10.0.0.66
ollama.prole.org 73.15.20.166
svc.prole.org 73.15.20.166
www.prole.org ghs.googlehosted.com

View File

@ -1,326 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# init_cloudnative_pg.sh
# Purpose:
# - Distribute administrator ed25519 key pair to CloudNativePG as a Kubernetes Secret for cert auth
# - Configure Kerberos (GSSAPI) using external Kerberos KDC
# - Patch CNPG cluster to enable TLS and GSSAPI where possible
#
# Usage:
# ./init_cloudnative_pg.sh start|stop|status|restart
# ./init_cloudnative_pg.sh initialize # create/update k8s secrets/configs and patch CNPG
# ./init_cloudnative_pg.sh update|reload # re-apply/patch
#
# Requirements:
# - init_openbao.sh has been run (OpenBao running in k8s)
# - $PROLE_HOME/env.sh or $HOME/.prole/env.sh defining PROLE_SERVICE
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Load env without leaking our positional args to the env script (some env.sh may `exec "$@"`).
__PROLE_SAVED_ARGS=("$@")
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$HOME/.prole/env.sh"
else
echo "ERROR: Missing env. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
exit 1
fi
set -- "${__PROLE_SAVED_ARGS[@]}"
unset __PROLE_SAVED_ARGS
if [[ -z "${PROLE_SERVICE:-}" ]]; then
echo "ERROR: PROLE_SERVICE is not defined in env." >&2
exit 1
fi
ACTION=${1:-}
CNPG_CLUSTER_NAME=${2:-${CNPG_CLUSTER_NAME:-prole-db}}
NAMESPACE=${NAMESPACE:-prole}
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
REALM=${REALM:-PROLE.ORG}
DOMAIN=${DOMAIN:-prole.org}
KRB5_KDC=${KRB5_KDC:-}
KRB5_ADMIN=${KRB5_ADMIN:-}
CNPG_MANIFEST="$SCRIPT_DIR/../k8s/prole/prole-db.yaml"
SECRETS_DIR="$PROLE_SERVICE/secrets"
ADMIN_PRIV="$SECRETS_DIR/admin_ed25519.key"
ADMIN_PUB="$SECRETS_DIR/admin_ed25519.pub"
OPENBAO_TOKEN_FILE="$SECRETS_DIR/openbao-root-token"
ensure_tools() {
for t in kubectl curl openssl base64 jq; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
# Resolve OpenBao URL: prefer explicit env, then localhost port-forward, then cluster DNS
bao_service_url() {
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
echo "$PROLE_OPENBAO_URL"
return 0
fi
# Prefer standard local port-forward managed by etc/init_port_forwards.sh
if curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:18200"
return 0
fi
echo "http://$OPENBAO_NAME.$NAMESPACE.svc.cluster.local:8200"
}
fetch_admin_keys_and_db_pass_from_bao_or_local() {
local token url
if [[ -f "$OPENBAO_TOKEN_FILE" ]]; then
token=$(cat "$OPENBAO_TOKEN_FILE")
else
token=""
fi
url=$(bao_service_url)
if [[ -n "$token" ]]; then
echo "Attempting to read admin key pair from OpenBao kv/prole/admin ..."
if curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -e '.data.data' >/dev/null 2>&1; then
local priv_b64 pub_b64
priv_b64=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -r '.data.data.admin_private_key_b64')
pub_b64=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -r '.data.data.admin_public_key_b64')
printf "%s" "$priv_b64" | base64 -d >"$ADMIN_PRIV"
printf "%s" "$pub_b64" | base64 -d >"$ADMIN_PUB"
chmod 0600 "$ADMIN_PRIV"
fi
echo "Attempting to read database password from OpenBao kv/prole/db ..."
if curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/db" | jq -e '.data.data' >/dev/null 2>&1; then
local db_pass
db_pass=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/db" | jq -r '.data.data.password')
if [[ -n "$db_pass" ]]; then
echo "Updating database user secret 'prole-db-user' from OpenBao ..."
kubectl create secret generic prole-db-user -n "$NAMESPACE" \
--from-literal=username=prole \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -f -
echo "Updating database superuser secret 'prole-db-superuser' from OpenBao ..."
kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \
--from-literal=username=postgres \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -f -
fi
fi
fi
if [[ -f "$ADMIN_PRIV" && -f "$ADMIN_PUB" ]]; then
echo "Using local admin key pair at $SECRETS_DIR"
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() {
echo "Creating/updating Secret cnpg-admin-key ..."
kubectl create secret generic cnpg-admin-key -n "$NAMESPACE" \
--from-file=admin.key="$ADMIN_PRIV" \
--from-file=admin.pub="$ADMIN_PUB" \
--dry-run=client -o yaml | kubectl apply -f -
}
ensure_krb5_conf_configmap() {
echo "Creating/updating ConfigMap prole-krb5-conf ..."
local kdc_val admin_val
if [[ -n "$KRB5_KDC" ]]; then
kdc_val="$KRB5_KDC"
admin_val=${KRB5_ADMIN:-$(echo "$KRB5_KDC" | cut -d, -f1)}
else
kdc_val="kdc.$DOMAIN"
admin_val="kdc.$DOMAIN"
fi
local TMP
TMP=$(mktemp)
cat >"$TMP" <<EOF
[libdefaults]
default_realm = $REALM
dns_lookup_realm = true
dns_lookup_kdc = true
[realms]
$REALM = {
kdc = $kdc_val
admin_server = $admin_val
}
[domain_realm]
.$DOMAIN = $REALM
$DOMAIN = $REALM
EOF
kubectl -n "$NAMESPACE" create configmap prole-krb5-conf --from-file=krb5.conf="$TMP" --dry-run=client -o yaml | kubectl apply -f -
rm -f "$TMP"
}
generate_tls_if_missing() {
local ca_secret_name="${CNPG_CLUSTER_NAME}-ca"
if kubectl -n "$NAMESPACE" get secret "$ca_secret_name" >/dev/null 2>&1; then
echo "CA secret $ca_secret_name already exists; skipping generation."
return 0
fi
echo "Generating self-signed CA (RSA 4096) for CNPG ..."
local TMPD
TMPD=$(mktemp -d)
openssl genrsa -out "$TMPD/ca.key" 4096
openssl req -x509 -new -key "$TMPD/ca.key" -out "$TMPD/ca.crt" -days 3650 -subj "/CN=Prole CNPG CA"
kubectl -n "$NAMESPACE" create secret generic "$ca_secret_name" \
--from-file=ca.crt="$TMPD/ca.crt" \
--from-file=ca.key="$TMPD/ca.key" \
--dry-run=client -o yaml | kubectl apply -f -
rm -rf "$TMPD"
}
patch_cnpg_cluster_for_auth() {
echo "Patching CNPG Cluster $CNPG_CLUSTER_NAME to enable GSSAPI and fix config (best-effort) ..."
# Best-effort patch; schema may vary with CNPG version.
# We remove krb_srvname as it is unrecognized in some PG 17 builds.
# We revert certificates to default to avoid operator TLS issues.
kubectl -n "$NAMESPACE" patch cluster "$CNPG_CLUSTER_NAME" --type merge -p "{
\"spec\": {
\"postgresql\": {
\"parameters\": {\"krb_srvname\": null},
\"pg_hba\": [
\"local all postgres trust\",
\"host all postgres all scram-sha-256\",
\"host all all all gss include_realm=1 krb_realm=$REALM\",
\"host all all all scram-sha-256\"
]
},
\"certificates\": {
\"serverCASecret\": null,
\"clientCASecret\": null
}
}
}" >/dev/null || echo "Note: patch may need adjustment for your CNPG version."
}
initialize() {
ensure_tools
# Ensure port-forward is running for OpenBao (dependency)
echo "Ensuring port-forward for OpenBao is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart openbao
fetch_admin_keys_and_db_pass_from_bao_or_local
apply_cnpg_admin_secret
ensure_krb5_conf_configmap
# generate_tls_if_missing
patch_cnpg_cluster_for_auth
# Ensure port-forward is running for Postgres (local access)
echo "Ensuring port-forward for Postgres is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart postgres
echo "Initialization complete for CNPG + Kerberos + cert artifacts."
}
update_reload() {
initialize
}
case "$ACTION" in
recreate)
ensure_tools
"$0" delete "$CNPG_CLUSTER_NAME"
"$0" create "$CNPG_CLUSTER_NAME"
;;
create)
ensure_tools
echo "Installing CloudNative-PG operator ..."
kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.27/releases/cnpg-1.27.0.yaml
echo "Creating CloudNative-PG cluster and resources for '$CNPG_CLUSTER_NAME' ..."
kubectl apply -k "$SCRIPT_DIR/../k8s/prole"
# Ensure prole-index-html exists for prole deployment readiness probe
if ! kubectl get configmap prole-index-html -n prole >/dev/null 2>&1; then
echo "Creating prole-index-html configmap..."
printf "<html><body><h1>Prole</h1></body></html>" > /tmp/index.html
kubectl create configmap prole-index-html --from-file=/tmp/index.html -n prole
rm /tmp/index.html
fi
# Ensure prole-nginx-tls exists (self-signed for dev)
if ! kubectl get secret prole-nginx-tls -n prole >/dev/null 2>&1; then
echo "Generating self-signed prole-nginx-tls for development..."
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /tmp/nginx-tls.key -out /tmp/nginx-tls.crt \
-subj "/CN=prole.org" >/dev/null 2>&1
kubectl create secret tls prole-nginx-tls --key /tmp/nginx-tls.key --cert /tmp/nginx-tls.crt -n prole
rm /tmp/nginx-tls.key /tmp/nginx-tls.crt
fi
echo "Initializing and patching cluster ..."
initialize
;;
delete)
ensure_tools
echo "Deleting all resources for '$CNPG_CLUSTER_NAME' ..."
kubectl delete -k "$SCRIPT_DIR/../k8s/prole" --ignore-not-found
;;
start)
ensure_tools
if [[ ! -f "$CNPG_MANIFEST" ]]; then
echo "ERROR: CNPG manifest not found at $CNPG_MANIFEST" >&2
exit 1
fi
echo "Starting CloudNative-PG cluster from $CNPG_MANIFEST in namespace $NAMESPACE..."
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST"
;;
stop)
ensure_tools
if [[ ! -f "$CNPG_MANIFEST" ]]; then
echo "ERROR: CNPG manifest not found at $CNPG_MANIFEST" >&2
exit 1
fi
echo "Stopping CloudNative-PG cluster using $CNPG_MANIFEST ..."
kubectl delete -f "$CNPG_MANIFEST" --ignore-not-found
;;
status)
ensure_tools
echo "--- CloudNative-PG Cluster Status ($CNPG_CLUSTER_NAME) ---"
if kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" >/dev/null 2>&1; then
kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME"
echo ""
echo "CNPG Plugin Status:"
if kubectl cnpg version >/dev/null 2>&1; then
kubectl cnpg status "$CNPG_CLUSTER_NAME" -n "$NAMESPACE"
else
echo "Note: 'kubectl cnpg' plugin not found; skipping detailed status."
fi
else
echo "Cluster '$CNPG_CLUSTER_NAME' not found in namespace '$NAMESPACE'."
fi
;;
restart)
ensure_tools
"$0" stop
"$0" start
;;
initialize)
initialize
;;
update|reload)
update_reload
;;
*)
echo "Usage: $0 {create|delete|recreate|start|stop|status|restart|initialize|update|reload} [dbname]" >&2
exit 2
;;
esac

View File

@ -1,203 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# etc/init_k8s.sh
# Purpose: Manage k3d/k8s environment
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Preserve positional args while sourcing env
__PROLE_SAVED_ARGS=("$@")
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$HOME/.prole/env.sh"
fi
set -- "${__PROLE_SAVED_ARGS[@]}"
unset __PROLE_SAVED_ARGS
# Default values
VERBOSE=false
ENVIRONMENT="prod"
HOST=""
CLUSTER_NAME_DEFAULT="prole-dev-cluster"
CLUSTER_PORT_DEFAULT="6443"
usage() {
cat <<EOF
Usage: $0 [initialize|start|stop|restart|status] [options]
Actions:
initialize Create a k3d cluster (dev) or fetch kubeconfig (service/prod)
start Start the k3d cluster
stop Stop the k3d cluster
restart Restart the k3d cluster
status Show status of Docker and k3d cluster
Options:
-v, --verbose Enable verbose output
-e, --environment dev|service|prod (default: $ENVIRONMENT)
-h, --host Remote host for service/prod environments
-n, --name k3d cluster name
EOF
exit 1
}
log() {
echo "[INFO] $*"
}
debug() {
if [[ "$VERBOSE" == "true" ]]; then
echo "[DEBUG] $*"
fi
}
# Parse options
POSITIONAL_ARGS=()
CLUSTER_NAME=""
while [[ $# -gt 0 ]]; do
case $1 in
-v|--verbose)
VERBOSE=true
shift
;;
-e|--environment)
ENVIRONMENT="$2"
shift 2
;;
-h|--host)
HOST="$2"
shift 2
;;
-n|--name)
CLUSTER_NAME="$2"
shift 2
;;
*)
POSITIONAL_ARGS+=("$1")
shift
;;
esac
done
set -- "${POSITIONAL_ARGS[@]}"
ACTION=${1:-}
if [[ -z "$ACTION" ]]; then
usage
fi
ensure_tools() {
for t in k3d docker; do
command -v "$t" >/dev/null || { echo "ERROR: Missing required tool: $t" >&2; exit 1; }
done
}
ensure_docker() {
if ! docker info >/dev/null 2>&1; then
echo "ERROR: Docker is not running." >&2
exit 1
fi
}
initialize() {
ensure_tools
ensure_docker
if [[ "$ENVIRONMENT" == "dev" ]]; then
if [[ -z "$CLUSTER_NAME" ]]; then
if [[ -t 0 ]]; then
read -p "Enter k3d cluster name [$CLUSTER_NAME_DEFAULT]: " CLUSTER_NAME
CLUSTER_NAME=${CLUSTER_NAME:-$CLUSTER_NAME_DEFAULT}
else
CLUSTER_NAME=$CLUSTER_NAME_DEFAULT
fi
fi
if k3d cluster list "$CLUSTER_NAME" >/dev/null 2>&1; then
log "Cluster '$CLUSTER_NAME' already exists."
else
log "Creating k3d cluster '$CLUSTER_NAME'..."
k3d cluster create "$CLUSTER_NAME" -p "0.0.0.0:$CLUSTER_PORT_DEFAULT:6443@server:0"
fi
else
if [[ -n "$HOST" ]]; then
log "Environment is $ENVIRONMENT. Host is $HOST."
log "TODO: Fetch kubeconfig from $HOST (placeholder)."
else
log "Environment is $ENVIRONMENT. Use -h|--host to specify the remote cluster host."
fi
fi
}
status() {
ensure_tools
log "Checking Docker status..."
if docker info >/dev/null 2>&1; then
echo "Docker is running."
else
echo "Docker is NOT running."
fi
log "Listing k3d clusters..."
k3d cluster list
}
get_cluster_name() {
# If a name was provided or exists in env, use it.
# Otherwise, if there is only one cluster, use it.
# Finally, use default.
if [[ -n "${CLUSTER_NAME:-}" ]]; then
echo "$CLUSTER_NAME"
return
fi
local clusters
clusters=$(k3d cluster list --no-headers | awk '{print $1}')
local count
count=$(echo "$clusters" | grep -c . || true)
if [[ "$count" -eq 1 ]]; then
echo "$clusters"
else
echo "$CLUSTER_NAME_DEFAULT"
fi
}
case "$ACTION" in
initialize)
initialize
;;
status)
status
;;
start)
ensure_tools
CLUSTER_NAME=$(get_cluster_name)
log "Starting k3d cluster '$CLUSTER_NAME'..."
k3d cluster start "$CLUSTER_NAME"
;;
stop)
ensure_tools
CLUSTER_NAME=$(get_cluster_name)
log "Stopping k3d cluster '$CLUSTER_NAME'..."
k3d cluster stop "$CLUSTER_NAME"
;;
restart)
ensure_tools
CLUSTER_NAME=$(get_cluster_name)
log "Restarting k3d cluster '$CLUSTER_NAME'..."
k3d cluster stop "$CLUSTER_NAME"
k3d cluster start "$CLUSTER_NAME"
;;
*)
usage
;;
esac

View File

@ -1,490 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# init_openbao.sh
# Purpose:
# - Deploy OpenBao to Kubernetes (dev mode) and store admin ed25519 key pair
# Generate and apply a Kerberos krb5.conf ConfigMap for an external realm
# - Local Docker helpers for OpenBao (optional)
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Preserve positional args while sourcing env
__PROLE_SAVED_ARGS=("$@")
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$HOME/.prole/env.sh"
fi
set -- "${__PROLE_SAVED_ARGS[@]}"
unset __PROLE_SAVED_ARGS
if [[ -z "${PROLE_SERVICE:-}" ]]; then
echo "ERROR: PROLE_SERVICE is not defined in env. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
exit 1
fi
ACTION=${1:-}
# Defaults
NAMESPACE=${NAMESPACE:-prole}
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
OPENBAO_MANIFEST_DIR="$SCRIPT_DIR/../k8s/openbao"
# Kerberos realm defaults
KRB5_REALM=${KRB5_REALM:-PROLE.ORG}
DOMAIN=${DOMAIN:-$(echo "$KRB5_REALM" | tr 'A-Z' 'a-z')}
KRB5_KDC=${KRB5_KDC:-kdc.$DOMAIN}
KRB5_ADMIN=${KRB5_ADMIN:-}
# Secrets and token locations
SECRETS_DIR="$PROLE_SERVICE/secrets"
mkdir -p "$SECRETS_DIR"
admin_key_priv="$SECRETS_DIR/admin_ed25519.key"
admin_key_pub="$SECRETS_DIR/admin_ed25519.pub"
root_token_file="$SECRETS_DIR/openbao-root-token"
ensure_tools() {
for t in kubectl curl openssl base64; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
ensure_docker() {
command -v docker >/dev/null || { echo "Missing required tool: docker" >&2; exit 1; }
}
ensure_admin_keypair() {
# Ensure admin ed25519 keypair exists and a root token is available
mkdir -p "$SECRETS_DIR"
if [[ ! -f "$admin_key_priv" || ! -f "$admin_key_pub" ]]; then
echo "Generating admin ed25519 keypair in $SECRETS_DIR ..."
openssl genpkey -algorithm ED25519 -out "$admin_key_priv"
openssl pkey -in "$admin_key_priv" -pubout -out "$admin_key_pub"
chmod 0600 "$admin_key_priv"
fi
# Ensure a root token exists for OpenBao dev server interactions
if [[ ! -f "$root_token_file" || ! -s "$root_token_file" ]]; then
openssl rand -hex 24 >"$root_token_file"
chmod 0600 "$root_token_file"
fi
}
generate_openbao_manifests() {
mkdir -p "$OPENBAO_MANIFEST_DIR"
# Only create a simple deployment if none exists; otherwise respect current file
local deploy="$OPENBAO_MANIFEST_DIR/deployment.yaml"
if [[ ! -f "$deploy" ]]; then
cat >"$deploy" <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: $OPENBAO_NAME
namespace: $NAMESPACE
spec:
replicas: 1
selector:
matchLabels:
app: $OPENBAO_NAME
template:
metadata:
labels:
app: $OPENBAO_NAME
spec:
containers:
- name: $OPENBAO_NAME
image: $OPENBAO_IMAGE
args: ["server","-dev","-dev-listen-address=0.0.0.0:8200"]
ports:
- containerPort: 8200
---
apiVersion: v1
kind: Service
metadata:
name: $OPENBAO_NAME
namespace: $NAMESPACE
spec:
selector:
app: $OPENBAO_NAME
ports:
- name: http
port: 8200
targetPort: 8200
EOF
fi
}
generate_kerberos_configmap() {
mkdir -p "$OPENBAO_MANIFEST_DIR"
local domain_lower
domain_lower=$(echo "$DOMAIN" | tr 'A-Z' 'a-z')
# Split KRB5_KDC on comma into bash array
local IFS=','
read -r -a __kdcs <<< "$KRB5_KDC"
local kdc_lines=""
local kdc
for kdc in "${__kdcs[@]}"; do
kdc_lines+=$' kdc = '"$kdc"$'\n'
done
local admin_server admin_line
admin_server=${KRB5_ADMIN:-"${__kdcs[0]:-}"}
admin_line=""
if [[ -n "$admin_server" ]]; then
# 8 spaces to remain within the YAML literal block indentation
admin_line=$' admin_server = '"$admin_server"$'\n'
fi
cat >"$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml" <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: prole-krb5-conf
namespace: $NAMESPACE
data:
krb5.conf: |
[libdefaults]
default_realm = $KRB5_REALM
dns_lookup_realm = true
dns_lookup_kdc = true
[realms]
$KRB5_REALM = {
${kdc_lines}${admin_line} }
[domain_realm]
.$domain_lower = $KRB5_REALM
$domain_lower = $KRB5_REALM
EOF
}
apply_k8s() {
echo "Applying OpenBao manifest to namespace '$NAMESPACE' ..."
if [[ -f "$SCRIPT_DIR/../k8s/prole/openbao-statefulset.yaml" ]]; then
kubectl apply -n "$NAMESPACE" -f "$SCRIPT_DIR/../k8s/prole/openbao-statefulset.yaml"
kubectl apply -n "$NAMESPACE" -f "$SCRIPT_DIR/../k8s/prole/openbao-service.yaml"
else
kubectl apply -n "$NAMESPACE" -f "$OPENBAO_MANIFEST_DIR/deployment.yaml"
fi
echo "Applying Kerberos ConfigMap (external realm) to namespace '$NAMESPACE' ..."
kubectl apply -n "$NAMESPACE" -f "$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml"
}
wait_for_openbao() {
echo "Waiting for OpenBao to become ready ..."
if kubectl get statefulset/$OPENBAO_NAME -n "$NAMESPACE" >/dev/null 2>&1; then
kubectl rollout status statefulset/$OPENBAO_NAME -n "$NAMESPACE" --timeout=120s
else
kubectl rollout status deploy/$OPENBAO_NAME -n "$NAMESPACE" --timeout=120s
fi
}
init_openbao_kv_and_store_admin_key() {
local token
token=$(cat "$root_token_file")
# Enable kv (if not already) and write keys
local svc="http://127.0.0.1:18200"
echo "Configuring KV at $svc ..."
# Check if kv/ is already mounted
if curl -sS -H "X-Vault-Token: $token" "$svc/v1/sys/mounts" | grep -q '"kv/":'; then
echo "KV path 'kv/' is already enabled."
else
curl -sS -H "X-Vault-Token: $token" -X POST "$svc/v1/sys/mounts/kv" \
-d '{"type":"kv","options":{"version":"2"}}' >/dev/null 2>&1 || true
fi
local priv pub
priv=$(base64 <"$admin_key_priv" | tr -d '\n')
pub=$(base64 <"$admin_key_pub" | tr -d '\n')
# Check if admin key already exists and matches
local existing_data
existing_data=$(curl -sS -H "X-Vault-Token: $token" "$svc/v1/kv/data/prole/admin" 2>/dev/null || true)
if [[ -n "$existing_data" ]]; then
local ex_priv ex_pub
ex_priv=$(echo "$existing_data" | grep -o '"admin_private_key_b64":"[^"]*' | cut -d'"' -f4 || true)
ex_pub=$(echo "$existing_data" | grep -o '"admin_public_key_b64":"[^"]*' | cut -d'"' -f4 || true)
if [[ "$ex_priv" == "$priv" && "$ex_pub" == "$pub" ]]; then
echo "Admin key pair in OpenBao kv/prole/admin is already up to date."
else
echo "Writing admin key pair to kv/prole/admin ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/admin" \
-d "{\"data\":{\"admin_private_key_b64\":\"$priv\",\"admin_public_key_b64\":\"$pub\"}}" >/dev/null
echo "Stored admin key pair in OpenBao kv/prole/admin."
fi
else
echo "Writing admin key pair to kv/prole/admin ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/admin" \
-d "{\"data\":{\"admin_private_key_b64\":\"$priv\",\"admin_public_key_b64\":\"$pub\"}}" >/dev/null
echo "Stored admin key pair in OpenBao kv/prole/admin."
fi
# Store User SSH keys if they exist
local user_key_priv="$HOME/.ssh/id_prole_ed25519"
local user_key_pub="$HOME/.ssh/id_prole_ed25519.pub"
if [[ -f "$user_key_priv" && -f "$user_key_pub" ]]; then
local u_priv u_pub
u_priv=$(base64 <"$user_key_priv" | tr -d '\n')
u_pub=$(base64 <"$user_key_pub" | tr -d '\n')
local username=${PROLE_DB_USER:-"prole"}
echo "Writing user SSH keys for '$username' to kv/prole/user ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/user" \
-d "{\"data\":{\"username\":\"$username\",\"private_key_b64\":\"$u_priv\",\"public_key_b64\":\"$u_pub\"}}" >/dev/null
echo "Stored user SSH keys in OpenBao kv/prole/user."
fi
if [[ -n "$db_pass" ]]; then
local username=${PROLE_DB_USER:-"prole"}
echo "Writing database user password for '$username' to kv/prole/db ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/db" \
-d "{\"data\":{\"username\":\"$username\",\"password\":\"$db_pass\"}}" >/dev/null
echo "Stored database password in OpenBao kv/prole/db."
fi
}
# Removed: prompt_admin_password_and_apply_secret (no AD admin secret required)
docker_start() {
echo "Starting local Docker container for OpenBao ..."
ensure_docker
docker rm -f "$OPENBAO_NAME" >/dev/null 2>&1 || true
# OpenBao dev
local token
token=$(cat "$root_token_file" 2>/dev/null || true)
if [[ -z "$token" ]]; then
token=$(openssl rand -hex 24)
printf "%s" "$token" >"$root_token_file"
chmod 0600 "$root_token_file"
fi
docker run -d --name "$OPENBAO_NAME" -p 18200:8200 \
"$OPENBAO_IMAGE" server -dev -dev-listen-address=0.0.0.0:8200 -dev-root-token-id="$token"
echo "Docker container started: $OPENBAO_NAME"
}
docker_stop() {
docker rm -f "$OPENBAO_NAME" >/dev/null 2>&1 || true
echo "Stopped OpenBao container if it was running."
}
docker_restart() {
docker_stop
docker_start
}
# status command implementation wrapped in a function to avoid top-level 'local'
cmd_status() {
echo "--- init_primary_domain status ---"
echo "Namespace: $NAMESPACE"
echo "OpenBao name: $OPENBAO_NAME"
echo "Manifest dir: $OPENBAO_MANIFEST_DIR"
# Files/manifests present
local ok=0
if [[ -f "$OPENBAO_MANIFEST_DIR/deployment.yaml" ]]; then
echo "[OK] OpenBao deployment manifest exists"
else
echo "[MISSING] $OPENBAO_MANIFEST_DIR/deployment.yaml"
ok=1
fi
if [[ -f "$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml" ]]; then
echo "[OK] Kerberos ConfigMap manifest exists"
else
echo "[MISSING] $OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml"
ok=1
fi
# K8s resources
if kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" >/dev/null 2>&1; then
local ready desired
ready=$(kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0")
desired=$(kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" -o jsonpath='{.status.replicas}' 2>/dev/null || echo "0")
ready=${ready:-0}
desired=${desired:-0}
if [[ "$ready" == "$desired" && "$ready" != "0" ]]; then
echo "[OK] OpenBao StatefulSet running ($ready/$desired ready)"
else
echo "[WARN] OpenBao StatefulSet not fully ready ($ready/$desired)"
ok=1
fi
elif kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" >/dev/null 2>&1; then
# Get readiness
local ready desired
ready=$(kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0")
desired=$(kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" -o jsonpath='{.status.replicas}' 2>/dev/null || echo "0")
ready=${ready:-0}
desired=${desired:-0}
if [[ "$ready" == "$desired" && "$ready" != "0" ]]; then
echo "[OK] OpenBao Deployment running ($ready/$desired ready)"
else
echo "[WARN] OpenBao Deployment not fully ready ($ready/$desired)"
ok=1
fi
else
echo "[MISSING] OpenBao Deployment or StatefulSet '$OPENBAO_NAME' in namespace '$NAMESPACE'"
ok=1
fi
if kubectl -n "$NAMESPACE" get configmap prole-krb5-conf >/dev/null 2>&1; then
echo "[OK] Kerberos ConfigMap 'prole-krb5-conf' present"
else
echo "[MISSING] Kerberos ConfigMap 'prole-krb5-conf' in namespace '$NAMESPACE'"
ok=1
fi
# Docker (optional local mode)
if command -v docker >/dev/null 2>&1; then
if docker ps --format '{{.Names}}' | grep -Fxq "$OPENBAO_NAME"; then
echo "[OK] Docker container '$OPENBAO_NAME' is running"
else
# Not necessarily an error; we primarily use k8s
echo "[INFO] Docker container '$OPENBAO_NAME' not running (k8s mode may be in use)"
fi
fi
# Secrets and tokens
if [[ -f "$admin_key_priv" && -f "$admin_key_pub" ]]; then
echo "[OK] Admin ed25519 keypair present in $SECRETS_DIR"
else
echo "[MISSING] Admin keypair files in $SECRETS_DIR"
ok=1
fi
if [[ -s "$root_token_file" ]]; then
echo "[OK] OpenBao root token file present"
else
echo "[MISSING] OpenBao root token file ($root_token_file)"
ok=1
fi
# OpenBao reachability and token validity (best-effort)
# Prefer explicit override, then localhost port-forward, then cluster DNS
local svc_url
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
svc_url="$PROLE_OPENBAO_URL"
elif curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then
svc_url="http://127.0.0.1:18200"
else
svc_url="http://$OPENBAO_NAME.$NAMESPACE.svc.cluster.local:8200"
fi
if curl -sS "$svc_url/v1/sys/health" >/dev/null 2>&1; then
echo "[OK] OpenBao service reachable"
if [[ -s "$root_token_file" ]]; then
local code
code=$(curl -s -o /dev/null -w "%{http_code}" -H "X-Vault-Token: $(cat "$root_token_file")" "$svc_url/v1/sys/mounts" || echo "000")
if [[ "$code" == "200" ]]; then
echo "[OK] OpenBao root token authenticates"
else
echo "[WARN] OpenBao root token did not authenticate (HTTP $code)"
ok=1
fi
fi
else
echo "[WARN] OpenBao service not reachable at $svc_url"
ok=1
fi
# Service certificate existence (CNPG TLS)
local CNPG_CLUSTER_NAME
CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db}
if kubectl -n "$NAMESPACE" get secret "${CNPG_CLUSTER_NAME}-tls" >/dev/null 2>&1; then
echo "[OK] Service certificate secret '${CNPG_CLUSTER_NAME}-tls' exists"
else
echo "[INFO] Service certificate secret '${CNPG_CLUSTER_NAME}-tls' not found"
fi
echo "-------------------------------------"
if [[ $ok -eq 0 ]]; then
echo "Status: OK"
return 0
else
echo "Status: Issues detected"
return 1
fi
}
case "$ACTION" in
start)
ensure_tools
ensure_docker
ensure_admin_keypair
docker_start
;;
status)
ensure_tools
cmd_status
;;
stop)
ensure_docker
docker_stop
;;
restart)
ensure_tools
ensure_docker
ensure_admin_keypair
docker_restart
;;
initialize)
ensure_tools
# Read password from stdin if provided
db_pass=""
if [[ ! -t 0 ]]; then
read -r db_pass
fi
ensure_admin_keypair
generate_openbao_manifests
generate_kerberos_configmap
apply_k8s
if [[ -n "$db_pass" ]]; then
local username=${PROLE_DB_USER:-"prole"}
echo "Creating database user secret 'prole-db-user' for '$username' ..."
kubectl create secret generic prole-db-user -n "$NAMESPACE" \
--from-literal=username="$username" \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -n "$NAMESPACE" -f -
echo "Creating database superuser secret 'prole-db-superuser' ..."
kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \
--from-literal=username=postgres \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -n "$NAMESPACE" -f -
fi
wait_for_openbao
# Ensure port-forward is running for OpenBao
echo "Ensuring port-forward for OpenBao is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart openbao
if ! curl -sS http://127.0.0.1:18200/v1/sys/health >/dev/null 2>&1; then
echo "ERROR: OpenBao not reachable at http://127.0.0.1:18200." >&2
echo "Please start port-forwards: $SCRIPT_DIR/init_port_forwards.sh start openbao" >&2
exit 1
fi
init_openbao_kv_and_store_admin_key
echo "Initialization complete. Manifests in: $OPENBAO_MANIFEST_DIR"
;;
update|reload)
generate_openbao_manifests
generate_kerberos_configmap
apply_k8s
echo "Re-applied manifests."
;;
*)
echo "Usage: $0 {start|stop|status|restart|initialize|update|reload}" >&2
exit 2
;;
esac

View File

@ -1,537 +0,0 @@
#!/usr/bin/env bash
set -u
# init_port_forwards.sh
# Portable-ish (macOS, Ubuntu, Raspberry Pi OS, Alpine) bash init-style script
# Manages kubectl port-forward daemons defined in an XML file.
PROG="init_port_forwards"
PROLE_HOME="${PROLE_HOME:-/Users/chrisfu/dev/prole}"
VERBOSE=0
CONFIG_FILE="$PROLE_HOME/conf/port-mappings.properties"
usage() {
cat <<EOF
Usage:
$PROG [-v|--verbose] [-c|--config-file=FILE] <start|stop|restart|status> [component]
Options:
-c, --config-file=FILE Path to local-ports.properties (XML)
-v, --verbose Verbose output
Examples:
$PROG -c ./port-mappings.properties start
$PROG stop openbao
$PROG --verbose status
EOF
}
TARGET_ID=""
log() { printf '%s\n' "$*"; }
vlog() { [ "$VERBOSE" -eq 1 ] && printf '[verbose] %s\n' "$*" || true; }
err() { printf '[error] %s\n' "$*" >&2; }
have() { command -v "$1" >/dev/null 2>&1; }
# Choose a writable state dir for pid/log files:
# - Prefer XDG_RUNTIME_DIR if set and writable
# - Else /var/run if writable (rare without root)
# - Else ~/.local/state
# - Else /tmp
state_dir() {
if [ -n "${XDG_RUNTIME_DIR:-}" ] && [ -w "${XDG_RUNTIME_DIR:-}" ]; then
printf '%s/%s' "$XDG_RUNTIME_DIR" "$PROG"
return
fi
if [ -d "/var/run" ] && [ -w "/var/run" ]; then
printf '/var/run/%s' "$PROG"
return
fi
if [ -n "${HOME:-}" ]; then
mkdir -p "$HOME/.local/state" >/dev/null 2>&1 || true
if [ -d "$HOME/.local/state" ] && [ -w "$HOME/.local/state" ]; then
printf '%s/.local/state/%s' "$HOME" "$PROG"
return
fi
fi
printf '/tmp/%s' "$PROG"
}
STATE_DIR="$(state_dir)"
PID_DIR="$STATE_DIR/pids"
LOG_DIR="$STATE_DIR/logs"
ensure_dirs() {
mkdir -p "$PID_DIR" "$LOG_DIR" 2>/dev/null || true
if [ ! -d "$PID_DIR" ] || [ ! -d "$LOG_DIR" ]; then
err "Unable to create state directories under: $STATE_DIR"
exit 1
fi
}
# Use kubectl for actions; kubecolor is used only for prettier status output.
KUBECTL="kubectl"
detect_kubectl() {
if have kubectl; then
KUBECTL="kubectl"
else
err "kubectl not found in PATH"
exit 1
fi
}
# Basic environment validation
validate_env() {
detect_kubectl
ensure_dirs
if ! "$KUBECTL" version --client >/dev/null 2>&1; then
err "kubectl seems broken or not executable"
exit 1
fi
# Can we reach the cluster?
if ! "$KUBECTL" cluster-info >/dev/null 2>&1; then
err "kubectl cannot reach a cluster (check KUBECONFIG/context)"
err "Try: kubectl config get-contexts && kubectl config use-context <ctx>"
exit 1
fi
}
# ---- Preflight: Docker + k3d awareness ----
# Return 0 when Docker CLI can talk to a running daemon.
docker_is_running() {
if ! have docker; then
return 1
fi
docker info >/dev/null 2>&1
}
# Hard-fail with clean guidance when Docker is not up (used for start/restart).
ensure_docker_running() {
if docker_is_running; then
return 0
fi
if ! have docker; then
err "Docker CLI not found. Please install Docker Desktop or docker CLI."
else
err "Docker is not running. Start Docker Desktop and wait until it is ready."
fi
err "Tip (macOS): open -a Docker"
err "Then re-run: $PROG start"
exit 3
}
# Detect k3d cluster name from env or current kubectl context.
# - If K3D_CLUSTER is set, use it.
# - Else, if current-context starts with 'k3d-', strip prefix to get name.
detect_k3d_context() {
if [ -n "${K3D_CLUSTER:-}" ]; then
printf '%s' "$K3D_CLUSTER"
return 0
fi
local ctx
ctx="$($KUBECTL config current-context 2>/dev/null || echo)"
case "$ctx" in
k3d-*) printf '%s' "${ctx#k3d-}"; return 0 ;;
*) return 1 ;;
esac
}
# Return 0 if the given k3d cluster exists and is running.
k3d_cluster_is_running() {
local name="$1"
have k3d || return 1
# Use json output when available; fall back to grep otherwise
if k3d cluster list -o json >/dev/null 2>&1; then
k3d cluster list -o json 2>/dev/null | grep -q '"name"\s*:\s*"'"$name"'"' && \
k3d cluster list -o json 2>/dev/null | sed -n 's/.*"name"\s*:\s*"\([^"]\+\)".*"serversRunning"\s*:\s*\([0-9]\+\).*/\1 \2/p' | awk -v n="$name" '$1==n {exit ($2>0)?0:1}'
return $?
else
k3d cluster list 2>/dev/null | grep -E "^$name\s" | grep -q running
return $?
fi
}
# If current context indicates k3d, ensure the cluster is up; otherwise no-op.
ensure_k3d_ready_if_applicable() {
local k3d_name
if ! k3d_name="$(detect_k3d_context)"; then
return 0
fi
if ! have k3d; then
err "k3d is not installed but kubectl context suggests k3d (context=$("$KUBECTL" config current-context))."
err "Install k3d: brew install k3d (macOS)"
err "Or switch context: kubectl config use-context <non-k3d-context>"
exit 4
fi
if ! k3d_cluster_is_running "$k3d_name"; then
err "k3d cluster '$k3d_name' is not running."
err "Start it: k3d cluster start $k3d_name"
err "Then re-run: $PROG start"
exit 4
fi
}
pid_file_for() { printf '%s/%s.pid' "$PID_DIR" "$1"; }
log_file_for() { printf '%s/%s.log' "$LOG_DIR" "$1"; }
is_pid_running() {
# Return 0 if pid exists and running, else 1
# kill -0 is portable.
local pid="$1"
[ -n "$pid" ] && kill -0 "$pid" >/dev/null 2>&1
}
read_pid() {
local pf="$1"
[ -f "$pf" ] || return 1
# shellcheck disable=SC2162
read pid <"$pf" || return 1
printf '%s' "$pid"
}
write_pid() {
local pf="$1" pid="$2"
printf '%s\n' "$pid" >"$pf"
}
remove_pidfile() {
local pf="$1"
rm -f "$pf" >/dev/null 2>&1 || true
}
# ---- XML parsing (simple attribute extraction) ----
# We parse lines containing: <mapping ... />
# Attributes must use double quotes in the XML (as in the sample).
get_attr() {
# $1 = line, $2 = attribute name
# outputs value or empty
printf '%s\n' "$1" | sed -n "s/.*$2=\"\([^\"]*\)\".*/\1/p"
}
foreach_mapping() {
# Calls a provided function with mapping fields:
# callback id ns target address hostPort servicePort protocol description
local callback="$1"
[ -f "$CONFIG_FILE" ] || { err "Config file not found: $CONFIG_FILE"; exit 1; }
# Support both single-line and multi-line self-closing mapping tags, e.g.:
# <mapping id="x" ... /> OR lines spanning multiple lines until "/>".
local in_mapping=0 in_comment=0 buffer="" line
while IFS= read -r line; do
# Handle XML comments: skip anything between <!-- and -->
if [ $in_comment -eq 1 ]; then
case "$line" in
*"-->"*) in_comment=0; continue ;;
*) continue ;;
esac
fi
case "$line" in
*"<!--"*)
case "$line" in
*"-->"*)
# single-line comment; skip line
continue
;;
*)
in_comment=1
continue
;;
esac
;;
esac
if [ $in_mapping -eq 0 ]; then
case "$line" in
*"<mapping"*)
in_mapping=1
buffer="$line"
;;
*)
continue
;;
esac
else
# Accumulate lines until we see the closing '/>'
buffer="$buffer $line"
fi
if [ $in_mapping -eq 1 ] && printf '%s' "$line" | grep -q "/>"; then
# Normalize whitespace to make attribute extraction robust
local merged
merged=$(printf '%s\n' "$buffer" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g')
local id ns target address hostPort servicePort protocol description
id="$(get_attr "$merged" "id")"
ns="$(get_attr "$merged" "namespace")"
target="$(get_attr "$merged" "target")"
address="$(get_attr "$merged" "address")"
hostPort="$(get_attr "$merged" "hostPort")"
servicePort="$(get_attr "$merged" "servicePort")"
protocol="$(get_attr "$merged" "protocol")"
description="$(get_attr "$merged" "description")"
# Basic validation
if [ -z "$id" ] || [ -z "$ns" ] || [ -z "$target" ] || [ -z "$hostPort" ] || [ -z "$servicePort" ]; then
err "Invalid mapping (missing required attributes): $merged"
exit 1
fi
# Filter by TARGET_ID if set
if [ -n "$TARGET_ID" ] && [ "$id" != "$TARGET_ID" ]; then
in_mapping=0
buffer=""
continue
fi
if [ -z "$address" ]; then address="127.0.0.1"; fi
if [ -z "$protocol" ]; then protocol="TCP"; fi
vlog "mapping: id=$id ns=$ns target=$target address=$address hostPort=$hostPort servicePort=$servicePort protocol=$protocol"
"$callback" "$id" "$ns" "$target" "$address" "$hostPort" "$servicePort" "$protocol" "$description"
# Reset accumulator
in_mapping=0
buffer=""
fi
done <"$CONFIG_FILE"
}
build_port_forward_cmd() {
# echo a command string
local ns="$1" target="$2" address="$3" hostPort="$4" servicePort="$5"
# kubectl port-forward -n <ns> --address <addr> <target> <local>:<remote>
printf '%s port-forward -n %s --address %s %s %s:%s' \
"$KUBECTL" "$ns" "$address" "$target" "$hostPort" "$servicePort"
}
start_port_forward() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
local pidfile logfile cmd pid
# Aggressively stop existing processes before starting to avoid port conflicts
stop_port_forward "$id" "$ns" "$target" "$address" "$hostPort" "$servicePort" "$protocol" "$description"
pidfile="$(pid_file_for "$id")"
logfile="$(log_file_for "$id")"
cmd="$(build_port_forward_cmd "$ns" "$target" "$address" "$hostPort" "$servicePort")"
log "Starting: $id $address:$hostPort -> $target:$servicePort ($ns) ${description:-}"
vlog "Command: $cmd"
# Start in background, keep output in log.
# nohup is available on macOS/Linux; redirect stdin from /dev/null to detach.
nohup sh -c "$cmd" >>"$logfile" 2>&1 </dev/null &
pid="$!"
write_pid "$pidfile" "$pid"
# Quick verification
sleep 0.2
if is_pid_running "$pid"; then
vlog "$id started (pid=$pid, log=$logfile)"
return 0
else
err "$id failed to start (see log: $logfile)"
remove_pidfile "$pidfile"
return 1
fi
}
stop_port_forward() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
local pidfile pid
pidfile="$(pid_file_for "$id")"
pid=""
if [ -f "$pidfile" ]; then
pid="$(read_pid "$pidfile" || true)"
fi
if [ -n "${pid:-}" ] && is_pid_running "$pid"; then
log "Stopping: $id (pid=$pid)"
kill "$pid" >/dev/null 2>&1 || true
# wait a moment, then SIGKILL if needed
local i
for i in 1 2 3 4 5; do
if ! is_pid_running "$pid"; then break; fi
sleep 0.2
done
if is_pid_running "$pid"; then
err "$id did not stop gracefully; sending SIGKILL"
kill -9 "$pid" >/dev/null 2>&1 || true
fi
else
if [ -f "$pidfile" ]; then
vlog "$id stale pidfile (pid=$pid not running)"
fi
fi
remove_pidfile "$pidfile"
# Aggressively remove any other matching kubectl port-forward processes
# Search for processes that match: kubectl port-forward -n <ns> ... <target> <hostPort>:<servicePort>
local extra_pids
extra_pids=$(ps -ef | grep "port-forward" | grep "\-n" | grep "$ns" | grep "$target" | grep "$hostPort:$servicePort" | grep -v grep | awk '{print $2}')
for epid in $extra_pids; do
if [ "$epid" != "$pid" ]; then
log "Cleaning up orphan process for $id (pid=$epid)"
kill -9 "$epid" >/dev/null 2>&1 || true
fi
done
}
status_one() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
local pidfile pid
pidfile="$(pid_file_for "$id")"
pid=""
if [ -f "$pidfile" ]; then
pid="$(read_pid "$pidfile" || true)"
fi
if [ -n "${pid:-}" ] && is_pid_running "$pid"; then
printf 'RUNNING %-12s pid=%-7s %s:%s -> %s:%s ns=%s %s\n' \
"$id" "$pid" "$address" "$hostPort" "$target" "$servicePort" "$ns" "${description:-}"
# ps details (portable flags vary; use a conservative format)
ps -p "$pid" -o pid=,ppid=,etime=,command= 2>/dev/null | sed 's/^/ /' || true
else
printf 'STOPPED %-12s (no live pid) %s:%s -> %s:%s ns=%s %s\n' \
"$id" "$address" "$hostPort" "$target" "$servicePort" "$ns" "${description:-}"
fi
}
do_start() {
validate_env
# Preflight: require Docker daemon and k3d (if applicable)
ensure_docker_running
ensure_k3d_ready_if_applicable
foreach_mapping start_port_forward
}
do_stop() {
# stop doesn't require cluster access, but it does need state dirs
ensure_dirs
foreach_mapping stop_port_forward
}
do_restart() {
validate_env
# Preflight: require Docker daemon and k3d (if applicable)
ensure_docker_running
ensure_k3d_ready_if_applicable
foreach_mapping stop_port_forward
foreach_mapping start_port_forward
}
do_status() {
validate_env
# Non-fatal awareness messages
if docker_is_running; then
log "[OK] Docker daemon is running"
else
if have docker; then
log "[WARN] Docker is not running"
else
log "[WARN] Docker CLI not found"
fi
fi
local _k3d_name
if _k3d_name="$(detect_k3d_context)"; then
if have k3d; then
if k3d_cluster_is_running "$_k3d_name"; then
log "[OK] k3d cluster '$_k3d_name' is running"
else
log "[WARN] k3d cluster '$_k3d_name' is not running"
fi
else
log "[WARN] k3d not installed but context suggests k3d (cluster='$_k3d_name')"
fi
fi
log "== Context / Cluster =="
"$KUBECTL" config current-context 2>/dev/null | sed 's/^/ context: /' || true
"$KUBECTL" cluster-info 2>/dev/null | sed 's/^/ /' || true
log ""
log "== Port-forward processes =="
foreach_mapping status_one
log ""
log "== Quick k3d awareness checks (best-effort) =="
# If user is on k3d, current-context often includes k3d-... but not guaranteed.
# Show nodes + a few namespaces/services related to mappings (best effort).
"$KUBECTL" get nodes -o wide 2>/dev/null | sed 's/^/ /' || true
log ""
"$KUBECTL" get ns 2>/dev/null | sed 's/^/ /' || true
log ""
# For each mapping, try to show target existence
log "== Target existence (best-effort) =="
_target_check() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
# kubectl get <target> -n <ns>
# If target is like "svc/name", "deploy/name", etc.
if "$KUBECTL" get -n "$ns" "$target" >/dev/null 2>&1; then
printf 'OK %-12s %s (ns=%s)\n' "$id" "$target" "$ns"
else
printf 'MISSING %-12s %s (ns=%s)\n' "$id" "$target" "$ns"
fi
}
foreach_mapping _target_check
}
# ---- arg parsing ----
ACTION=""
while [ $# -gt 0 ]; do
case "$1" in
start|stop|restart|status)
if [ -z "$ACTION" ]; then
ACTION="$1"
else
TARGET_ID="$1"
fi
shift
;;
-v|--verbose)
VERBOSE=1
shift
;;
-c)
shift
[ $# -gt 0 ] || { err "-c requires a file path"; usage; exit 2; }
CONFIG_FILE="$1"
shift
;;
--config-file=*)
CONFIG_FILE="${1#*=}"
shift
;;
-h|--help)
usage
exit 0
;;
*)
if [ -z "$ACTION" ]; then
err "Unknown arg: $1"
usage
exit 2
fi
TARGET_ID="$1"
shift
;;
esac
done
[ -n "$ACTION" ] || { usage; exit 2; }
case "$ACTION" in
start) do_start ;;
stop) do_stop ;;
restart) do_restart ;;
status) do_status ;;
esac

View File

@ -1,215 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# init_prole-db.sh
# Purpose:
# - Manage prole-db CloudNative-PG cluster operations (deploy, start, stop, etc.)
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ACTION=${1:-}
VERSION=${2:-latest}
# Load env
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
source "$HOME/.prole/env.sh"
fi
CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db}
NAMESPACE=${NAMESPACE:-prole}
CNPG_MANIFEST="$SCRIPT_DIR/../k8s/prole/prole-db.yaml"
ensure_tools() {
for t in kubectl jq; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
get_latest_image() {
local version_file="$SCRIPT_DIR/../conf/postgresql/.version"
if [[ -f "$version_file" ]]; then
echo "prole-db:$(cat "$version_file" | tr -d '[:space:]')"
else
echo "prole-db:17.7-033"
fi
}
start() {
ensure_tools
echo "Checking dependencies..."
# 1. k3d is running
if ! command -v k3d >/dev/null || ! k3d cluster list >/dev/null 2>&1; then
echo "ERROR: k3d is not running or not installed." >&2
exit 1
fi
# 2. cnpg operator is loaded
if ! kubectl get deployment -n cnpg-system cnpg-controller-manager >/dev/null 2>&1; then
echo "ERROR: CloudNative-PG operator is not loaded." >&2
exit 1
fi
# 3. openbao is configured
if ! kubectl get statefulset openbao -n "$NAMESPACE" >/dev/null 2>&1 && ! kubectl get deployment openbao -n "$NAMESPACE" >/dev/null 2>&1; then
echo "ERROR: OpenBao is not deployed." >&2
exit 1
fi
# Compare latest image with deployed
local latest_image
latest_image=$(get_latest_image)
local current_image
current_image=$(kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.imageName}' 2>/dev/null || echo "")
if [[ "$current_image" != "$latest_image" ]]; then
echo "Updating cluster image from '$current_image' to '$latest_image'..."
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{\"spec\": {\"imageName\": \"$latest_image\"}}"
# Force a rollout to ensure the new image is pulled even if it was just a tag update (though we use unique tags)
kubectl cnpg restart "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" || true
else
echo "Cluster is already using the latest image: $latest_image"
fi
echo "Starting prole-db cluster (ensuring manifest is applied)..."
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST"
}
stop() {
ensure_tools
echo "Stopping prole-db cluster $CNPG_CLUSTER_NAME..."
# Identify instances
local instances
instances=$(kubectl get pods -n "$NAMESPACE" -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" -o jsonpath='{.items[*].metadata.name}')
if [[ -z "$instances" ]]; then
echo "No instances found for cluster $CNPG_CLUSTER_NAME."
return
fi
# Determine replicas and primary
local primary
primary=$(kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" -o jsonpath='{.status.currentPrimary}')
echo "Primary: $primary"
# Shutdown replicas first
for pod in $instances; do
if [[ "$pod" != "$primary" ]]; then
echo "Shutting down replica $pod..."
kubectl exec -n "$NAMESPACE" "$pod" -c postgres -- psql -U postgres -c "CHECKPOINT;" || true
fi
done
# Shutdown primary last
if [[ -n "$primary" ]]; then
echo "Shutting down primary $primary..."
kubectl exec -n "$NAMESPACE" "$primary" -c postgres -- psql -U postgres -c "CHECKPOINT;" || true
fi
echo "Deleting cluster resource..."
kubectl delete -f "$CNPG_MANIFEST" --ignore-not-found
}
restart() {
ensure_tools
echo "Restarting prole-db cluster..."
kubectl cnpg restart "$CNPG_CLUSTER_NAME" -n "$NAMESPACE"
}
deploy() {
ensure_tools
local image
if [[ "$VERSION" == "latest" ]]; then
image=$(get_latest_image)
else
image="prole-db:$VERSION"
fi
echo "Deploying $image to cluster $CNPG_CLUSTER_NAME..."
# If cluster doesn't exist, use init_cloudnative_pg.sh create first
if ! kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" >/dev/null 2>&1; then
echo "Cluster not found. Running init_cloudnative_pg.sh create..."
bash "$SCRIPT_DIR/init_cloudnative_pg.sh" create
fi
# Ensure image is updated if manifest has an older version
# First, apply the manifest to ensure the cluster exists/is updated
echo "Applying manifest $CNPG_MANIFEST..."
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST"
# Then force the specific image version via patch if different
local current_image
current_image=$(kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.imageName}' 2>/dev/null || echo "")
if [[ "$current_image" != "$image" ]]; then
echo "Patching cluster to use image '$image' (was '$current_image')..."
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{\"spec\": {\"imageName\": \"$image\"}}"
# Force a rollout
kubectl cnpg restart "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" || true
fi
}
rollback() {
ensure_tools
echo "Rollback: Updating to previous image (manually specified version or fallback)..."
if [[ "$VERSION" == "latest" ]]; then
echo "Please specify a version to rollback to. Usage: $0 rollback <version>"
exit 1
fi
deploy
}
backup() {
echo "Backup - tbd, when we configure s3 or other block store"
}
reset() {
ensure_tools
echo "Resetting prole-db cluster..."
echo "Removing cnpg instances and pods..."
kubectl delete cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --ignore-not-found
# Wait for deletion
kubectl wait --for=delete cluster/"$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --timeout=60s || true
echo "Reinstantiating..."
deploy
}
case "$ACTION" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
deploy)
deploy
;;
rollback)
rollback
;;
backup)
backup
;;
reset)
reset
;;
*)
echo "Usage: $0 {start|stop|restart|deploy|rollback|backup|reset} [version]" >&2
exit 2
;;
esac

View File

@ -1,29 +0,0 @@
#!/usr/bin/env bash
# Prole environment configuration
# This file is generated by the installer. Source it in new shells, or execute as a wrapper:
# "$PROLE_HOME/env.sh" <command> [args…]
# shellcheck shell=bash
export PROLE_HOME="<MagicMock name='mock.Entry().get().strip()' id='4494452656'>"
export PROLE_CONF="<MagicMock name='mock.Entry().get().strip()' id='4494452656'>"
export PROLE_DATA="<MagicMock name='mock.Entry().get().strip()' id='4494452656'>"
export PROLE_LOGS="<MagicMock name='mock.Entry().get().strip()' id='4494452656'>"
export PROLE_SERVICE="<MagicMock name='mock.Entry().get().strip()' id='4494452656'>"
# Ensure PATH works for GUI-launched shells (Docker, Ollama, etc.)
_prole_add_path() { case ":${PATH}:" in *":$1:"*) ;; *) PATH="$1:${PATH:-}" ;; esac; }
_prole_add_path "$PROLE_HOME/bin"
_prole_add_path "/opt/homebrew/bin"
_prole_add_path "/usr/local/bin"
_prole_add_path "/usr/bin"
_prole_add_path "/bin"
_prole_add_path "/usr/sbin"
_prole_add_path "/sbin"
export PATH
# Add custom paths below if needed (examples):
# _prole_add_path "/Applications/Ollama.app/Contents/MacOS"
# If executed with arguments, run them under this environment
if [ "$#" -gt 0 ]; then
exec "$@"
fi

View File

@ -1,15 +0,0 @@
myrddin.prole.org 10.0.0.3
raspberry.prole.org 10.0.0.4
pi.prole.org 10.0.0.5
synology.prole.org 10.0.0.203
morgoth.prole.org 10.0.0.204
zinfandel.prole.org 10.0.0.205
aventage.prole.org 10.0.0.206
retropie.prole.org 10.0.0.207
fairyland.prole.org 10.0.0.208
k8s.prole.org zinfandel.prole.org
mc.prole.org 73.15.20.166
morana.prole.org 10.0.0.66
ollama.prole.org 73.15.20.166
svc.prole.org 73.15.20.166
www.prole.org ghs.googlehosted.com

View File

@ -1,326 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# init_cloudnative_pg.sh
# Purpose:
# - Distribute administrator ed25519 key pair to CloudNativePG as a Kubernetes Secret for cert auth
# - Configure Kerberos (GSSAPI) using external Kerberos KDC
# - Patch CNPG cluster to enable TLS and GSSAPI where possible
#
# Usage:
# ./init_cloudnative_pg.sh start|stop|status|restart
# ./init_cloudnative_pg.sh initialize # create/update k8s secrets/configs and patch CNPG
# ./init_cloudnative_pg.sh update|reload # re-apply/patch
#
# Requirements:
# - init_openbao.sh has been run (OpenBao running in k8s)
# - $PROLE_HOME/env.sh or $HOME/.prole/env.sh defining PROLE_SERVICE
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Load env without leaking our positional args to the env script (some env.sh may `exec "$@"`).
__PROLE_SAVED_ARGS=("$@")
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$HOME/.prole/env.sh"
else
echo "ERROR: Missing env. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
exit 1
fi
set -- "${__PROLE_SAVED_ARGS[@]}"
unset __PROLE_SAVED_ARGS
if [[ -z "${PROLE_SERVICE:-}" ]]; then
echo "ERROR: PROLE_SERVICE is not defined in env." >&2
exit 1
fi
ACTION=${1:-}
CNPG_CLUSTER_NAME=${2:-${CNPG_CLUSTER_NAME:-prole-db}}
NAMESPACE=${NAMESPACE:-prole}
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
REALM=${REALM:-PROLE.ORG}
DOMAIN=${DOMAIN:-prole.org}
KRB5_KDC=${KRB5_KDC:-}
KRB5_ADMIN=${KRB5_ADMIN:-}
CNPG_MANIFEST="$SCRIPT_DIR/../k8s/prole/prole-db.yaml"
SECRETS_DIR="$PROLE_SERVICE/secrets"
ADMIN_PRIV="$SECRETS_DIR/admin_ed25519.key"
ADMIN_PUB="$SECRETS_DIR/admin_ed25519.pub"
OPENBAO_TOKEN_FILE="$SECRETS_DIR/openbao-root-token"
ensure_tools() {
for t in kubectl curl openssl base64 jq; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
# Resolve OpenBao URL: prefer explicit env, then localhost port-forward, then cluster DNS
bao_service_url() {
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
echo "$PROLE_OPENBAO_URL"
return 0
fi
# Prefer standard local port-forward managed by etc/init_port_forwards.sh
if curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then
echo "http://127.0.0.1:18200"
return 0
fi
echo "http://$OPENBAO_NAME.$NAMESPACE.svc.cluster.local:8200"
}
fetch_admin_keys_and_db_pass_from_bao_or_local() {
local token url
if [[ -f "$OPENBAO_TOKEN_FILE" ]]; then
token=$(cat "$OPENBAO_TOKEN_FILE")
else
token=""
fi
url=$(bao_service_url)
if [[ -n "$token" ]]; then
echo "Attempting to read admin key pair from OpenBao kv/prole/admin ..."
if curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -e '.data.data' >/dev/null 2>&1; then
local priv_b64 pub_b64
priv_b64=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -r '.data.data.admin_private_key_b64')
pub_b64=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -r '.data.data.admin_public_key_b64')
printf "%s" "$priv_b64" | base64 -d >"$ADMIN_PRIV"
printf "%s" "$pub_b64" | base64 -d >"$ADMIN_PUB"
chmod 0600 "$ADMIN_PRIV"
fi
echo "Attempting to read database password from OpenBao kv/prole/db ..."
if curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/db" | jq -e '.data.data' >/dev/null 2>&1; then
local db_pass
db_pass=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/db" | jq -r '.data.data.password')
if [[ -n "$db_pass" ]]; then
echo "Updating database user secret 'prole-db-user' from OpenBao ..."
kubectl create secret generic prole-db-user -n "$NAMESPACE" \
--from-literal=username=prole \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -f -
echo "Updating database superuser secret 'prole-db-superuser' from OpenBao ..."
kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \
--from-literal=username=postgres \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -f -
fi
fi
fi
if [[ -f "$ADMIN_PRIV" && -f "$ADMIN_PUB" ]]; then
echo "Using local admin key pair at $SECRETS_DIR"
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() {
echo "Creating/updating Secret cnpg-admin-key ..."
kubectl create secret generic cnpg-admin-key -n "$NAMESPACE" \
--from-file=admin.key="$ADMIN_PRIV" \
--from-file=admin.pub="$ADMIN_PUB" \
--dry-run=client -o yaml | kubectl apply -f -
}
ensure_krb5_conf_configmap() {
echo "Creating/updating ConfigMap prole-krb5-conf ..."
local kdc_val admin_val
if [[ -n "$KRB5_KDC" ]]; then
kdc_val="$KRB5_KDC"
admin_val=${KRB5_ADMIN:-$(echo "$KRB5_KDC" | cut -d, -f1)}
else
kdc_val="kdc.$DOMAIN"
admin_val="kdc.$DOMAIN"
fi
local TMP
TMP=$(mktemp)
cat >"$TMP" <<EOF
[libdefaults]
default_realm = $REALM
dns_lookup_realm = true
dns_lookup_kdc = true
[realms]
$REALM = {
kdc = $kdc_val
admin_server = $admin_val
}
[domain_realm]
.$DOMAIN = $REALM
$DOMAIN = $REALM
EOF
kubectl -n "$NAMESPACE" create configmap prole-krb5-conf --from-file=krb5.conf="$TMP" --dry-run=client -o yaml | kubectl apply -f -
rm -f "$TMP"
}
generate_tls_if_missing() {
local ca_secret_name="${CNPG_CLUSTER_NAME}-ca"
if kubectl -n "$NAMESPACE" get secret "$ca_secret_name" >/dev/null 2>&1; then
echo "CA secret $ca_secret_name already exists; skipping generation."
return 0
fi
echo "Generating self-signed CA (RSA 4096) for CNPG ..."
local TMPD
TMPD=$(mktemp -d)
openssl genrsa -out "$TMPD/ca.key" 4096
openssl req -x509 -new -key "$TMPD/ca.key" -out "$TMPD/ca.crt" -days 3650 -subj "/CN=Prole CNPG CA"
kubectl -n "$NAMESPACE" create secret generic "$ca_secret_name" \
--from-file=ca.crt="$TMPD/ca.crt" \
--from-file=ca.key="$TMPD/ca.key" \
--dry-run=client -o yaml | kubectl apply -f -
rm -rf "$TMPD"
}
patch_cnpg_cluster_for_auth() {
echo "Patching CNPG Cluster $CNPG_CLUSTER_NAME to enable GSSAPI and fix config (best-effort) ..."
# Best-effort patch; schema may vary with CNPG version.
# We remove krb_srvname as it is unrecognized in some PG 17 builds.
# We revert certificates to default to avoid operator TLS issues.
kubectl -n "$NAMESPACE" patch cluster "$CNPG_CLUSTER_NAME" --type merge -p "{
\"spec\": {
\"postgresql\": {
\"parameters\": {\"krb_srvname\": null},
\"pg_hba\": [
\"local all postgres trust\",
\"host all postgres all scram-sha-256\",
\"host all all all gss include_realm=1 krb_realm=$REALM\",
\"host all all all scram-sha-256\"
]
},
\"certificates\": {
\"serverCASecret\": null,
\"clientCASecret\": null
}
}
}" >/dev/null || echo "Note: patch may need adjustment for your CNPG version."
}
initialize() {
ensure_tools
# Ensure port-forward is running for OpenBao (dependency)
echo "Ensuring port-forward for OpenBao is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart openbao
fetch_admin_keys_and_db_pass_from_bao_or_local
apply_cnpg_admin_secret
ensure_krb5_conf_configmap
# generate_tls_if_missing
patch_cnpg_cluster_for_auth
# Ensure port-forward is running for Postgres (local access)
echo "Ensuring port-forward for Postgres is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart postgres
echo "Initialization complete for CNPG + Kerberos + cert artifacts."
}
update_reload() {
initialize
}
case "$ACTION" in
recreate)
ensure_tools
"$0" delete "$CNPG_CLUSTER_NAME"
"$0" create "$CNPG_CLUSTER_NAME"
;;
create)
ensure_tools
echo "Installing CloudNative-PG operator ..."
kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.27/releases/cnpg-1.27.0.yaml
echo "Creating CloudNative-PG cluster and resources for '$CNPG_CLUSTER_NAME' ..."
kubectl apply -k "$SCRIPT_DIR/../k8s/prole"
# Ensure prole-index-html exists for prole deployment readiness probe
if ! kubectl get configmap prole-index-html -n prole >/dev/null 2>&1; then
echo "Creating prole-index-html configmap..."
printf "<html><body><h1>Prole</h1></body></html>" > /tmp/index.html
kubectl create configmap prole-index-html --from-file=/tmp/index.html -n prole
rm /tmp/index.html
fi
# Ensure prole-nginx-tls exists (self-signed for dev)
if ! kubectl get secret prole-nginx-tls -n prole >/dev/null 2>&1; then
echo "Generating self-signed prole-nginx-tls for development..."
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /tmp/nginx-tls.key -out /tmp/nginx-tls.crt \
-subj "/CN=prole.org" >/dev/null 2>&1
kubectl create secret tls prole-nginx-tls --key /tmp/nginx-tls.key --cert /tmp/nginx-tls.crt -n prole
rm /tmp/nginx-tls.key /tmp/nginx-tls.crt
fi
echo "Initializing and patching cluster ..."
initialize
;;
delete)
ensure_tools
echo "Deleting all resources for '$CNPG_CLUSTER_NAME' ..."
kubectl delete -k "$SCRIPT_DIR/../k8s/prole" --ignore-not-found
;;
start)
ensure_tools
if [[ ! -f "$CNPG_MANIFEST" ]]; then
echo "ERROR: CNPG manifest not found at $CNPG_MANIFEST" >&2
exit 1
fi
echo "Starting CloudNative-PG cluster from $CNPG_MANIFEST in namespace $NAMESPACE..."
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST"
;;
stop)
ensure_tools
if [[ ! -f "$CNPG_MANIFEST" ]]; then
echo "ERROR: CNPG manifest not found at $CNPG_MANIFEST" >&2
exit 1
fi
echo "Stopping CloudNative-PG cluster using $CNPG_MANIFEST ..."
kubectl delete -f "$CNPG_MANIFEST" --ignore-not-found
;;
status)
ensure_tools
echo "--- CloudNative-PG Cluster Status ($CNPG_CLUSTER_NAME) ---"
if kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" >/dev/null 2>&1; then
kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME"
echo ""
echo "CNPG Plugin Status:"
if kubectl cnpg version >/dev/null 2>&1; then
kubectl cnpg status "$CNPG_CLUSTER_NAME" -n "$NAMESPACE"
else
echo "Note: 'kubectl cnpg' plugin not found; skipping detailed status."
fi
else
echo "Cluster '$CNPG_CLUSTER_NAME' not found in namespace '$NAMESPACE'."
fi
;;
restart)
ensure_tools
"$0" stop
"$0" start
;;
initialize)
initialize
;;
update|reload)
update_reload
;;
*)
echo "Usage: $0 {create|delete|recreate|start|stop|status|restart|initialize|update|reload} [dbname]" >&2
exit 2
;;
esac

View File

@ -1,203 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# etc/init_k8s.sh
# Purpose: Manage k3d/k8s environment
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Preserve positional args while sourcing env
__PROLE_SAVED_ARGS=("$@")
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$HOME/.prole/env.sh"
fi
set -- "${__PROLE_SAVED_ARGS[@]}"
unset __PROLE_SAVED_ARGS
# Default values
VERBOSE=false
ENVIRONMENT="prod"
HOST=""
CLUSTER_NAME_DEFAULT="prole-dev-cluster"
CLUSTER_PORT_DEFAULT="6443"
usage() {
cat <<EOF
Usage: $0 [initialize|start|stop|restart|status] [options]
Actions:
initialize Create a k3d cluster (dev) or fetch kubeconfig (service/prod)
start Start the k3d cluster
stop Stop the k3d cluster
restart Restart the k3d cluster
status Show status of Docker and k3d cluster
Options:
-v, --verbose Enable verbose output
-e, --environment dev|service|prod (default: $ENVIRONMENT)
-h, --host Remote host for service/prod environments
-n, --name k3d cluster name
EOF
exit 1
}
log() {
echo "[INFO] $*"
}
debug() {
if [[ "$VERBOSE" == "true" ]]; then
echo "[DEBUG] $*"
fi
}
# Parse options
POSITIONAL_ARGS=()
CLUSTER_NAME=""
while [[ $# -gt 0 ]]; do
case $1 in
-v|--verbose)
VERBOSE=true
shift
;;
-e|--environment)
ENVIRONMENT="$2"
shift 2
;;
-h|--host)
HOST="$2"
shift 2
;;
-n|--name)
CLUSTER_NAME="$2"
shift 2
;;
*)
POSITIONAL_ARGS+=("$1")
shift
;;
esac
done
set -- "${POSITIONAL_ARGS[@]}"
ACTION=${1:-}
if [[ -z "$ACTION" ]]; then
usage
fi
ensure_tools() {
for t in k3d docker; do
command -v "$t" >/dev/null || { echo "ERROR: Missing required tool: $t" >&2; exit 1; }
done
}
ensure_docker() {
if ! docker info >/dev/null 2>&1; then
echo "ERROR: Docker is not running." >&2
exit 1
fi
}
initialize() {
ensure_tools
ensure_docker
if [[ "$ENVIRONMENT" == "dev" ]]; then
if [[ -z "$CLUSTER_NAME" ]]; then
if [[ -t 0 ]]; then
read -p "Enter k3d cluster name [$CLUSTER_NAME_DEFAULT]: " CLUSTER_NAME
CLUSTER_NAME=${CLUSTER_NAME:-$CLUSTER_NAME_DEFAULT}
else
CLUSTER_NAME=$CLUSTER_NAME_DEFAULT
fi
fi
if k3d cluster list "$CLUSTER_NAME" >/dev/null 2>&1; then
log "Cluster '$CLUSTER_NAME' already exists."
else
log "Creating k3d cluster '$CLUSTER_NAME'..."
k3d cluster create "$CLUSTER_NAME" -p "0.0.0.0:$CLUSTER_PORT_DEFAULT:6443@server:0"
fi
else
if [[ -n "$HOST" ]]; then
log "Environment is $ENVIRONMENT. Host is $HOST."
log "TODO: Fetch kubeconfig from $HOST (placeholder)."
else
log "Environment is $ENVIRONMENT. Use -h|--host to specify the remote cluster host."
fi
fi
}
status() {
ensure_tools
log "Checking Docker status..."
if docker info >/dev/null 2>&1; then
echo "Docker is running."
else
echo "Docker is NOT running."
fi
log "Listing k3d clusters..."
k3d cluster list
}
get_cluster_name() {
# If a name was provided or exists in env, use it.
# Otherwise, if there is only one cluster, use it.
# Finally, use default.
if [[ -n "${CLUSTER_NAME:-}" ]]; then
echo "$CLUSTER_NAME"
return
fi
local clusters
clusters=$(k3d cluster list --no-headers | awk '{print $1}')
local count
count=$(echo "$clusters" | grep -c . || true)
if [[ "$count" -eq 1 ]]; then
echo "$clusters"
else
echo "$CLUSTER_NAME_DEFAULT"
fi
}
case "$ACTION" in
initialize)
initialize
;;
status)
status
;;
start)
ensure_tools
CLUSTER_NAME=$(get_cluster_name)
log "Starting k3d cluster '$CLUSTER_NAME'..."
k3d cluster start "$CLUSTER_NAME"
;;
stop)
ensure_tools
CLUSTER_NAME=$(get_cluster_name)
log "Stopping k3d cluster '$CLUSTER_NAME'..."
k3d cluster stop "$CLUSTER_NAME"
;;
restart)
ensure_tools
CLUSTER_NAME=$(get_cluster_name)
log "Restarting k3d cluster '$CLUSTER_NAME'..."
k3d cluster stop "$CLUSTER_NAME"
k3d cluster start "$CLUSTER_NAME"
;;
*)
usage
;;
esac

View File

@ -1,490 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# init_openbao.sh
# Purpose:
# - Deploy OpenBao to Kubernetes (dev mode) and store admin ed25519 key pair
# Generate and apply a Kerberos krb5.conf ConfigMap for an external realm
# - Local Docker helpers for OpenBao (optional)
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Preserve positional args while sourcing env
__PROLE_SAVED_ARGS=("$@")
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
# shellcheck disable=SC1090
source "$HOME/.prole/env.sh"
fi
set -- "${__PROLE_SAVED_ARGS[@]}"
unset __PROLE_SAVED_ARGS
if [[ -z "${PROLE_SERVICE:-}" ]]; then
echo "ERROR: PROLE_SERVICE is not defined in env. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
exit 1
fi
ACTION=${1:-}
# Defaults
NAMESPACE=${NAMESPACE:-prole}
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
OPENBAO_MANIFEST_DIR="$SCRIPT_DIR/../k8s/openbao"
# Kerberos realm defaults
KRB5_REALM=${KRB5_REALM:-PROLE.ORG}
DOMAIN=${DOMAIN:-$(echo "$KRB5_REALM" | tr 'A-Z' 'a-z')}
KRB5_KDC=${KRB5_KDC:-kdc.$DOMAIN}
KRB5_ADMIN=${KRB5_ADMIN:-}
# Secrets and token locations
SECRETS_DIR="$PROLE_SERVICE/secrets"
mkdir -p "$SECRETS_DIR"
admin_key_priv="$SECRETS_DIR/admin_ed25519.key"
admin_key_pub="$SECRETS_DIR/admin_ed25519.pub"
root_token_file="$SECRETS_DIR/openbao-root-token"
ensure_tools() {
for t in kubectl curl openssl base64; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
ensure_docker() {
command -v docker >/dev/null || { echo "Missing required tool: docker" >&2; exit 1; }
}
ensure_admin_keypair() {
# Ensure admin ed25519 keypair exists and a root token is available
mkdir -p "$SECRETS_DIR"
if [[ ! -f "$admin_key_priv" || ! -f "$admin_key_pub" ]]; then
echo "Generating admin ed25519 keypair in $SECRETS_DIR ..."
openssl genpkey -algorithm ED25519 -out "$admin_key_priv"
openssl pkey -in "$admin_key_priv" -pubout -out "$admin_key_pub"
chmod 0600 "$admin_key_priv"
fi
# Ensure a root token exists for OpenBao dev server interactions
if [[ ! -f "$root_token_file" || ! -s "$root_token_file" ]]; then
openssl rand -hex 24 >"$root_token_file"
chmod 0600 "$root_token_file"
fi
}
generate_openbao_manifests() {
mkdir -p "$OPENBAO_MANIFEST_DIR"
# Only create a simple deployment if none exists; otherwise respect current file
local deploy="$OPENBAO_MANIFEST_DIR/deployment.yaml"
if [[ ! -f "$deploy" ]]; then
cat >"$deploy" <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: $OPENBAO_NAME
namespace: $NAMESPACE
spec:
replicas: 1
selector:
matchLabels:
app: $OPENBAO_NAME
template:
metadata:
labels:
app: $OPENBAO_NAME
spec:
containers:
- name: $OPENBAO_NAME
image: $OPENBAO_IMAGE
args: ["server","-dev","-dev-listen-address=0.0.0.0:8200"]
ports:
- containerPort: 8200
---
apiVersion: v1
kind: Service
metadata:
name: $OPENBAO_NAME
namespace: $NAMESPACE
spec:
selector:
app: $OPENBAO_NAME
ports:
- name: http
port: 8200
targetPort: 8200
EOF
fi
}
generate_kerberos_configmap() {
mkdir -p "$OPENBAO_MANIFEST_DIR"
local domain_lower
domain_lower=$(echo "$DOMAIN" | tr 'A-Z' 'a-z')
# Split KRB5_KDC on comma into bash array
local IFS=','
read -r -a __kdcs <<< "$KRB5_KDC"
local kdc_lines=""
local kdc
for kdc in "${__kdcs[@]}"; do
kdc_lines+=$' kdc = '"$kdc"$'\n'
done
local admin_server admin_line
admin_server=${KRB5_ADMIN:-"${__kdcs[0]:-}"}
admin_line=""
if [[ -n "$admin_server" ]]; then
# 8 spaces to remain within the YAML literal block indentation
admin_line=$' admin_server = '"$admin_server"$'\n'
fi
cat >"$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml" <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: prole-krb5-conf
namespace: $NAMESPACE
data:
krb5.conf: |
[libdefaults]
default_realm = $KRB5_REALM
dns_lookup_realm = true
dns_lookup_kdc = true
[realms]
$KRB5_REALM = {
${kdc_lines}${admin_line} }
[domain_realm]
.$domain_lower = $KRB5_REALM
$domain_lower = $KRB5_REALM
EOF
}
apply_k8s() {
echo "Applying OpenBao manifest to namespace '$NAMESPACE' ..."
if [[ -f "$SCRIPT_DIR/../k8s/prole/openbao-statefulset.yaml" ]]; then
kubectl apply -n "$NAMESPACE" -f "$SCRIPT_DIR/../k8s/prole/openbao-statefulset.yaml"
kubectl apply -n "$NAMESPACE" -f "$SCRIPT_DIR/../k8s/prole/openbao-service.yaml"
else
kubectl apply -n "$NAMESPACE" -f "$OPENBAO_MANIFEST_DIR/deployment.yaml"
fi
echo "Applying Kerberos ConfigMap (external realm) to namespace '$NAMESPACE' ..."
kubectl apply -n "$NAMESPACE" -f "$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml"
}
wait_for_openbao() {
echo "Waiting for OpenBao to become ready ..."
if kubectl get statefulset/$OPENBAO_NAME -n "$NAMESPACE" >/dev/null 2>&1; then
kubectl rollout status statefulset/$OPENBAO_NAME -n "$NAMESPACE" --timeout=120s
else
kubectl rollout status deploy/$OPENBAO_NAME -n "$NAMESPACE" --timeout=120s
fi
}
init_openbao_kv_and_store_admin_key() {
local token
token=$(cat "$root_token_file")
# Enable kv (if not already) and write keys
local svc="http://127.0.0.1:18200"
echo "Configuring KV at $svc ..."
# Check if kv/ is already mounted
if curl -sS -H "X-Vault-Token: $token" "$svc/v1/sys/mounts" | grep -q '"kv/":'; then
echo "KV path 'kv/' is already enabled."
else
curl -sS -H "X-Vault-Token: $token" -X POST "$svc/v1/sys/mounts/kv" \
-d '{"type":"kv","options":{"version":"2"}}' >/dev/null 2>&1 || true
fi
local priv pub
priv=$(base64 <"$admin_key_priv" | tr -d '\n')
pub=$(base64 <"$admin_key_pub" | tr -d '\n')
# Check if admin key already exists and matches
local existing_data
existing_data=$(curl -sS -H "X-Vault-Token: $token" "$svc/v1/kv/data/prole/admin" 2>/dev/null || true)
if [[ -n "$existing_data" ]]; then
local ex_priv ex_pub
ex_priv=$(echo "$existing_data" | grep -o '"admin_private_key_b64":"[^"]*' | cut -d'"' -f4 || true)
ex_pub=$(echo "$existing_data" | grep -o '"admin_public_key_b64":"[^"]*' | cut -d'"' -f4 || true)
if [[ "$ex_priv" == "$priv" && "$ex_pub" == "$pub" ]]; then
echo "Admin key pair in OpenBao kv/prole/admin is already up to date."
else
echo "Writing admin key pair to kv/prole/admin ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/admin" \
-d "{\"data\":{\"admin_private_key_b64\":\"$priv\",\"admin_public_key_b64\":\"$pub\"}}" >/dev/null
echo "Stored admin key pair in OpenBao kv/prole/admin."
fi
else
echo "Writing admin key pair to kv/prole/admin ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/admin" \
-d "{\"data\":{\"admin_private_key_b64\":\"$priv\",\"admin_public_key_b64\":\"$pub\"}}" >/dev/null
echo "Stored admin key pair in OpenBao kv/prole/admin."
fi
# Store User SSH keys if they exist
local user_key_priv="$HOME/.ssh/id_prole_ed25519"
local user_key_pub="$HOME/.ssh/id_prole_ed25519.pub"
if [[ -f "$user_key_priv" && -f "$user_key_pub" ]]; then
local u_priv u_pub
u_priv=$(base64 <"$user_key_priv" | tr -d '\n')
u_pub=$(base64 <"$user_key_pub" | tr -d '\n')
local username=${PROLE_DB_USER:-"prole"}
echo "Writing user SSH keys for '$username' to kv/prole/user ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/user" \
-d "{\"data\":{\"username\":\"$username\",\"private_key_b64\":\"$u_priv\",\"public_key_b64\":\"$u_pub\"}}" >/dev/null
echo "Stored user SSH keys in OpenBao kv/prole/user."
fi
if [[ -n "$db_pass" ]]; then
local username=${PROLE_DB_USER:-"prole"}
echo "Writing database user password for '$username' to kv/prole/db ..."
curl -sS -H "X-Vault-Token: $token" -H 'Content-Type: application/json' \
-X POST "$svc/v1/kv/data/prole/db" \
-d "{\"data\":{\"username\":\"$username\",\"password\":\"$db_pass\"}}" >/dev/null
echo "Stored database password in OpenBao kv/prole/db."
fi
}
# Removed: prompt_admin_password_and_apply_secret (no AD admin secret required)
docker_start() {
echo "Starting local Docker container for OpenBao ..."
ensure_docker
docker rm -f "$OPENBAO_NAME" >/dev/null 2>&1 || true
# OpenBao dev
local token
token=$(cat "$root_token_file" 2>/dev/null || true)
if [[ -z "$token" ]]; then
token=$(openssl rand -hex 24)
printf "%s" "$token" >"$root_token_file"
chmod 0600 "$root_token_file"
fi
docker run -d --name "$OPENBAO_NAME" -p 18200:8200 \
"$OPENBAO_IMAGE" server -dev -dev-listen-address=0.0.0.0:8200 -dev-root-token-id="$token"
echo "Docker container started: $OPENBAO_NAME"
}
docker_stop() {
docker rm -f "$OPENBAO_NAME" >/dev/null 2>&1 || true
echo "Stopped OpenBao container if it was running."
}
docker_restart() {
docker_stop
docker_start
}
# status command implementation wrapped in a function to avoid top-level 'local'
cmd_status() {
echo "--- init_primary_domain status ---"
echo "Namespace: $NAMESPACE"
echo "OpenBao name: $OPENBAO_NAME"
echo "Manifest dir: $OPENBAO_MANIFEST_DIR"
# Files/manifests present
local ok=0
if [[ -f "$OPENBAO_MANIFEST_DIR/deployment.yaml" ]]; then
echo "[OK] OpenBao deployment manifest exists"
else
echo "[MISSING] $OPENBAO_MANIFEST_DIR/deployment.yaml"
ok=1
fi
if [[ -f "$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml" ]]; then
echo "[OK] Kerberos ConfigMap manifest exists"
else
echo "[MISSING] $OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml"
ok=1
fi
# K8s resources
if kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" >/dev/null 2>&1; then
local ready desired
ready=$(kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0")
desired=$(kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" -o jsonpath='{.status.replicas}' 2>/dev/null || echo "0")
ready=${ready:-0}
desired=${desired:-0}
if [[ "$ready" == "$desired" && "$ready" != "0" ]]; then
echo "[OK] OpenBao StatefulSet running ($ready/$desired ready)"
else
echo "[WARN] OpenBao StatefulSet not fully ready ($ready/$desired)"
ok=1
fi
elif kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" >/dev/null 2>&1; then
# Get readiness
local ready desired
ready=$(kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0")
desired=$(kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" -o jsonpath='{.status.replicas}' 2>/dev/null || echo "0")
ready=${ready:-0}
desired=${desired:-0}
if [[ "$ready" == "$desired" && "$ready" != "0" ]]; then
echo "[OK] OpenBao Deployment running ($ready/$desired ready)"
else
echo "[WARN] OpenBao Deployment not fully ready ($ready/$desired)"
ok=1
fi
else
echo "[MISSING] OpenBao Deployment or StatefulSet '$OPENBAO_NAME' in namespace '$NAMESPACE'"
ok=1
fi
if kubectl -n "$NAMESPACE" get configmap prole-krb5-conf >/dev/null 2>&1; then
echo "[OK] Kerberos ConfigMap 'prole-krb5-conf' present"
else
echo "[MISSING] Kerberos ConfigMap 'prole-krb5-conf' in namespace '$NAMESPACE'"
ok=1
fi
# Docker (optional local mode)
if command -v docker >/dev/null 2>&1; then
if docker ps --format '{{.Names}}' | grep -Fxq "$OPENBAO_NAME"; then
echo "[OK] Docker container '$OPENBAO_NAME' is running"
else
# Not necessarily an error; we primarily use k8s
echo "[INFO] Docker container '$OPENBAO_NAME' not running (k8s mode may be in use)"
fi
fi
# Secrets and tokens
if [[ -f "$admin_key_priv" && -f "$admin_key_pub" ]]; then
echo "[OK] Admin ed25519 keypair present in $SECRETS_DIR"
else
echo "[MISSING] Admin keypair files in $SECRETS_DIR"
ok=1
fi
if [[ -s "$root_token_file" ]]; then
echo "[OK] OpenBao root token file present"
else
echo "[MISSING] OpenBao root token file ($root_token_file)"
ok=1
fi
# OpenBao reachability and token validity (best-effort)
# Prefer explicit override, then localhost port-forward, then cluster DNS
local svc_url
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
svc_url="$PROLE_OPENBAO_URL"
elif curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then
svc_url="http://127.0.0.1:18200"
else
svc_url="http://$OPENBAO_NAME.$NAMESPACE.svc.cluster.local:8200"
fi
if curl -sS "$svc_url/v1/sys/health" >/dev/null 2>&1; then
echo "[OK] OpenBao service reachable"
if [[ -s "$root_token_file" ]]; then
local code
code=$(curl -s -o /dev/null -w "%{http_code}" -H "X-Vault-Token: $(cat "$root_token_file")" "$svc_url/v1/sys/mounts" || echo "000")
if [[ "$code" == "200" ]]; then
echo "[OK] OpenBao root token authenticates"
else
echo "[WARN] OpenBao root token did not authenticate (HTTP $code)"
ok=1
fi
fi
else
echo "[WARN] OpenBao service not reachable at $svc_url"
ok=1
fi
# Service certificate existence (CNPG TLS)
local CNPG_CLUSTER_NAME
CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db}
if kubectl -n "$NAMESPACE" get secret "${CNPG_CLUSTER_NAME}-tls" >/dev/null 2>&1; then
echo "[OK] Service certificate secret '${CNPG_CLUSTER_NAME}-tls' exists"
else
echo "[INFO] Service certificate secret '${CNPG_CLUSTER_NAME}-tls' not found"
fi
echo "-------------------------------------"
if [[ $ok -eq 0 ]]; then
echo "Status: OK"
return 0
else
echo "Status: Issues detected"
return 1
fi
}
case "$ACTION" in
start)
ensure_tools
ensure_docker
ensure_admin_keypair
docker_start
;;
status)
ensure_tools
cmd_status
;;
stop)
ensure_docker
docker_stop
;;
restart)
ensure_tools
ensure_docker
ensure_admin_keypair
docker_restart
;;
initialize)
ensure_tools
# Read password from stdin if provided
db_pass=""
if [[ ! -t 0 ]]; then
read -r db_pass
fi
ensure_admin_keypair
generate_openbao_manifests
generate_kerberos_configmap
apply_k8s
if [[ -n "$db_pass" ]]; then
local username=${PROLE_DB_USER:-"prole"}
echo "Creating database user secret 'prole-db-user' for '$username' ..."
kubectl create secret generic prole-db-user -n "$NAMESPACE" \
--from-literal=username="$username" \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -n "$NAMESPACE" -f -
echo "Creating database superuser secret 'prole-db-superuser' ..."
kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \
--from-literal=username=postgres \
--from-literal=password="$db_pass" \
--dry-run=client -o yaml | kubectl apply -n "$NAMESPACE" -f -
fi
wait_for_openbao
# Ensure port-forward is running for OpenBao
echo "Ensuring port-forward for OpenBao is active ..."
"$SCRIPT_DIR/init_port_forwards.sh" restart openbao
if ! curl -sS http://127.0.0.1:18200/v1/sys/health >/dev/null 2>&1; then
echo "ERROR: OpenBao not reachable at http://127.0.0.1:18200." >&2
echo "Please start port-forwards: $SCRIPT_DIR/init_port_forwards.sh start openbao" >&2
exit 1
fi
init_openbao_kv_and_store_admin_key
echo "Initialization complete. Manifests in: $OPENBAO_MANIFEST_DIR"
;;
update|reload)
generate_openbao_manifests
generate_kerberos_configmap
apply_k8s
echo "Re-applied manifests."
;;
*)
echo "Usage: $0 {start|stop|status|restart|initialize|update|reload}" >&2
exit 2
;;
esac

View File

@ -1,537 +0,0 @@
#!/usr/bin/env bash
set -u
# init_port_forwards.sh
# Portable-ish (macOS, Ubuntu, Raspberry Pi OS, Alpine) bash init-style script
# Manages kubectl port-forward daemons defined in an XML file.
PROG="init_port_forwards"
PROLE_HOME="${PROLE_HOME:-/Users/chrisfu/dev/prole}"
VERBOSE=0
CONFIG_FILE="$PROLE_HOME/conf/port-mappings.properties"
usage() {
cat <<EOF
Usage:
$PROG [-v|--verbose] [-c|--config-file=FILE] <start|stop|restart|status> [component]
Options:
-c, --config-file=FILE Path to local-ports.properties (XML)
-v, --verbose Verbose output
Examples:
$PROG -c ./port-mappings.properties start
$PROG stop openbao
$PROG --verbose status
EOF
}
TARGET_ID=""
log() { printf '%s\n' "$*"; }
vlog() { [ "$VERBOSE" -eq 1 ] && printf '[verbose] %s\n' "$*" || true; }
err() { printf '[error] %s\n' "$*" >&2; }
have() { command -v "$1" >/dev/null 2>&1; }
# Choose a writable state dir for pid/log files:
# - Prefer XDG_RUNTIME_DIR if set and writable
# - Else /var/run if writable (rare without root)
# - Else ~/.local/state
# - Else /tmp
state_dir() {
if [ -n "${XDG_RUNTIME_DIR:-}" ] && [ -w "${XDG_RUNTIME_DIR:-}" ]; then
printf '%s/%s' "$XDG_RUNTIME_DIR" "$PROG"
return
fi
if [ -d "/var/run" ] && [ -w "/var/run" ]; then
printf '/var/run/%s' "$PROG"
return
fi
if [ -n "${HOME:-}" ]; then
mkdir -p "$HOME/.local/state" >/dev/null 2>&1 || true
if [ -d "$HOME/.local/state" ] && [ -w "$HOME/.local/state" ]; then
printf '%s/.local/state/%s' "$HOME" "$PROG"
return
fi
fi
printf '/tmp/%s' "$PROG"
}
STATE_DIR="$(state_dir)"
PID_DIR="$STATE_DIR/pids"
LOG_DIR="$STATE_DIR/logs"
ensure_dirs() {
mkdir -p "$PID_DIR" "$LOG_DIR" 2>/dev/null || true
if [ ! -d "$PID_DIR" ] || [ ! -d "$LOG_DIR" ]; then
err "Unable to create state directories under: $STATE_DIR"
exit 1
fi
}
# Use kubectl for actions; kubecolor is used only for prettier status output.
KUBECTL="kubectl"
detect_kubectl() {
if have kubectl; then
KUBECTL="kubectl"
else
err "kubectl not found in PATH"
exit 1
fi
}
# Basic environment validation
validate_env() {
detect_kubectl
ensure_dirs
if ! "$KUBECTL" version --client >/dev/null 2>&1; then
err "kubectl seems broken or not executable"
exit 1
fi
# Can we reach the cluster?
if ! "$KUBECTL" cluster-info >/dev/null 2>&1; then
err "kubectl cannot reach a cluster (check KUBECONFIG/context)"
err "Try: kubectl config get-contexts && kubectl config use-context <ctx>"
exit 1
fi
}
# ---- Preflight: Docker + k3d awareness ----
# Return 0 when Docker CLI can talk to a running daemon.
docker_is_running() {
if ! have docker; then
return 1
fi
docker info >/dev/null 2>&1
}
# Hard-fail with clean guidance when Docker is not up (used for start/restart).
ensure_docker_running() {
if docker_is_running; then
return 0
fi
if ! have docker; then
err "Docker CLI not found. Please install Docker Desktop or docker CLI."
else
err "Docker is not running. Start Docker Desktop and wait until it is ready."
fi
err "Tip (macOS): open -a Docker"
err "Then re-run: $PROG start"
exit 3
}
# Detect k3d cluster name from env or current kubectl context.
# - If K3D_CLUSTER is set, use it.
# - Else, if current-context starts with 'k3d-', strip prefix to get name.
detect_k3d_context() {
if [ -n "${K3D_CLUSTER:-}" ]; then
printf '%s' "$K3D_CLUSTER"
return 0
fi
local ctx
ctx="$($KUBECTL config current-context 2>/dev/null || echo)"
case "$ctx" in
k3d-*) printf '%s' "${ctx#k3d-}"; return 0 ;;
*) return 1 ;;
esac
}
# Return 0 if the given k3d cluster exists and is running.
k3d_cluster_is_running() {
local name="$1"
have k3d || return 1
# Use json output when available; fall back to grep otherwise
if k3d cluster list -o json >/dev/null 2>&1; then
k3d cluster list -o json 2>/dev/null | grep -q '"name"\s*:\s*"'"$name"'"' && \
k3d cluster list -o json 2>/dev/null | sed -n 's/.*"name"\s*:\s*"\([^"]\+\)".*"serversRunning"\s*:\s*\([0-9]\+\).*/\1 \2/p' | awk -v n="$name" '$1==n {exit ($2>0)?0:1}'
return $?
else
k3d cluster list 2>/dev/null | grep -E "^$name\s" | grep -q running
return $?
fi
}
# If current context indicates k3d, ensure the cluster is up; otherwise no-op.
ensure_k3d_ready_if_applicable() {
local k3d_name
if ! k3d_name="$(detect_k3d_context)"; then
return 0
fi
if ! have k3d; then
err "k3d is not installed but kubectl context suggests k3d (context=$("$KUBECTL" config current-context))."
err "Install k3d: brew install k3d (macOS)"
err "Or switch context: kubectl config use-context <non-k3d-context>"
exit 4
fi
if ! k3d_cluster_is_running "$k3d_name"; then
err "k3d cluster '$k3d_name' is not running."
err "Start it: k3d cluster start $k3d_name"
err "Then re-run: $PROG start"
exit 4
fi
}
pid_file_for() { printf '%s/%s.pid' "$PID_DIR" "$1"; }
log_file_for() { printf '%s/%s.log' "$LOG_DIR" "$1"; }
is_pid_running() {
# Return 0 if pid exists and running, else 1
# kill -0 is portable.
local pid="$1"
[ -n "$pid" ] && kill -0 "$pid" >/dev/null 2>&1
}
read_pid() {
local pf="$1"
[ -f "$pf" ] || return 1
# shellcheck disable=SC2162
read pid <"$pf" || return 1
printf '%s' "$pid"
}
write_pid() {
local pf="$1" pid="$2"
printf '%s\n' "$pid" >"$pf"
}
remove_pidfile() {
local pf="$1"
rm -f "$pf" >/dev/null 2>&1 || true
}
# ---- XML parsing (simple attribute extraction) ----
# We parse lines containing: <mapping ... />
# Attributes must use double quotes in the XML (as in the sample).
get_attr() {
# $1 = line, $2 = attribute name
# outputs value or empty
printf '%s\n' "$1" | sed -n "s/.*$2=\"\([^\"]*\)\".*/\1/p"
}
foreach_mapping() {
# Calls a provided function with mapping fields:
# callback id ns target address hostPort servicePort protocol description
local callback="$1"
[ -f "$CONFIG_FILE" ] || { err "Config file not found: $CONFIG_FILE"; exit 1; }
# Support both single-line and multi-line self-closing mapping tags, e.g.:
# <mapping id="x" ... /> OR lines spanning multiple lines until "/>".
local in_mapping=0 in_comment=0 buffer="" line
while IFS= read -r line; do
# Handle XML comments: skip anything between <!-- and -->
if [ $in_comment -eq 1 ]; then
case "$line" in
*"-->"*) in_comment=0; continue ;;
*) continue ;;
esac
fi
case "$line" in
*"<!--"*)
case "$line" in
*"-->"*)
# single-line comment; skip line
continue
;;
*)
in_comment=1
continue
;;
esac
;;
esac
if [ $in_mapping -eq 0 ]; then
case "$line" in
*"<mapping"*)
in_mapping=1
buffer="$line"
;;
*)
continue
;;
esac
else
# Accumulate lines until we see the closing '/>'
buffer="$buffer $line"
fi
if [ $in_mapping -eq 1 ] && printf '%s' "$line" | grep -q "/>"; then
# Normalize whitespace to make attribute extraction robust
local merged
merged=$(printf '%s\n' "$buffer" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g')
local id ns target address hostPort servicePort protocol description
id="$(get_attr "$merged" "id")"
ns="$(get_attr "$merged" "namespace")"
target="$(get_attr "$merged" "target")"
address="$(get_attr "$merged" "address")"
hostPort="$(get_attr "$merged" "hostPort")"
servicePort="$(get_attr "$merged" "servicePort")"
protocol="$(get_attr "$merged" "protocol")"
description="$(get_attr "$merged" "description")"
# Basic validation
if [ -z "$id" ] || [ -z "$ns" ] || [ -z "$target" ] || [ -z "$hostPort" ] || [ -z "$servicePort" ]; then
err "Invalid mapping (missing required attributes): $merged"
exit 1
fi
# Filter by TARGET_ID if set
if [ -n "$TARGET_ID" ] && [ "$id" != "$TARGET_ID" ]; then
in_mapping=0
buffer=""
continue
fi
if [ -z "$address" ]; then address="127.0.0.1"; fi
if [ -z "$protocol" ]; then protocol="TCP"; fi
vlog "mapping: id=$id ns=$ns target=$target address=$address hostPort=$hostPort servicePort=$servicePort protocol=$protocol"
"$callback" "$id" "$ns" "$target" "$address" "$hostPort" "$servicePort" "$protocol" "$description"
# Reset accumulator
in_mapping=0
buffer=""
fi
done <"$CONFIG_FILE"
}
build_port_forward_cmd() {
# echo a command string
local ns="$1" target="$2" address="$3" hostPort="$4" servicePort="$5"
# kubectl port-forward -n <ns> --address <addr> <target> <local>:<remote>
printf '%s port-forward -n %s --address %s %s %s:%s' \
"$KUBECTL" "$ns" "$address" "$target" "$hostPort" "$servicePort"
}
start_port_forward() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
local pidfile logfile cmd pid
# Aggressively stop existing processes before starting to avoid port conflicts
stop_port_forward "$id" "$ns" "$target" "$address" "$hostPort" "$servicePort" "$protocol" "$description"
pidfile="$(pid_file_for "$id")"
logfile="$(log_file_for "$id")"
cmd="$(build_port_forward_cmd "$ns" "$target" "$address" "$hostPort" "$servicePort")"
log "Starting: $id $address:$hostPort -> $target:$servicePort ($ns) ${description:-}"
vlog "Command: $cmd"
# Start in background, keep output in log.
# nohup is available on macOS/Linux; redirect stdin from /dev/null to detach.
nohup sh -c "$cmd" >>"$logfile" 2>&1 </dev/null &
pid="$!"
write_pid "$pidfile" "$pid"
# Quick verification
sleep 0.2
if is_pid_running "$pid"; then
vlog "$id started (pid=$pid, log=$logfile)"
return 0
else
err "$id failed to start (see log: $logfile)"
remove_pidfile "$pidfile"
return 1
fi
}
stop_port_forward() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
local pidfile pid
pidfile="$(pid_file_for "$id")"
pid=""
if [ -f "$pidfile" ]; then
pid="$(read_pid "$pidfile" || true)"
fi
if [ -n "${pid:-}" ] && is_pid_running "$pid"; then
log "Stopping: $id (pid=$pid)"
kill "$pid" >/dev/null 2>&1 || true
# wait a moment, then SIGKILL if needed
local i
for i in 1 2 3 4 5; do
if ! is_pid_running "$pid"; then break; fi
sleep 0.2
done
if is_pid_running "$pid"; then
err "$id did not stop gracefully; sending SIGKILL"
kill -9 "$pid" >/dev/null 2>&1 || true
fi
else
if [ -f "$pidfile" ]; then
vlog "$id stale pidfile (pid=$pid not running)"
fi
fi
remove_pidfile "$pidfile"
# Aggressively remove any other matching kubectl port-forward processes
# Search for processes that match: kubectl port-forward -n <ns> ... <target> <hostPort>:<servicePort>
local extra_pids
extra_pids=$(ps -ef | grep "port-forward" | grep "\-n" | grep "$ns" | grep "$target" | grep "$hostPort:$servicePort" | grep -v grep | awk '{print $2}')
for epid in $extra_pids; do
if [ "$epid" != "$pid" ]; then
log "Cleaning up orphan process for $id (pid=$epid)"
kill -9 "$epid" >/dev/null 2>&1 || true
fi
done
}
status_one() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
local pidfile pid
pidfile="$(pid_file_for "$id")"
pid=""
if [ -f "$pidfile" ]; then
pid="$(read_pid "$pidfile" || true)"
fi
if [ -n "${pid:-}" ] && is_pid_running "$pid"; then
printf 'RUNNING %-12s pid=%-7s %s:%s -> %s:%s ns=%s %s\n' \
"$id" "$pid" "$address" "$hostPort" "$target" "$servicePort" "$ns" "${description:-}"
# ps details (portable flags vary; use a conservative format)
ps -p "$pid" -o pid=,ppid=,etime=,command= 2>/dev/null | sed 's/^/ /' || true
else
printf 'STOPPED %-12s (no live pid) %s:%s -> %s:%s ns=%s %s\n' \
"$id" "$address" "$hostPort" "$target" "$servicePort" "$ns" "${description:-}"
fi
}
do_start() {
validate_env
# Preflight: require Docker daemon and k3d (if applicable)
ensure_docker_running
ensure_k3d_ready_if_applicable
foreach_mapping start_port_forward
}
do_stop() {
# stop doesn't require cluster access, but it does need state dirs
ensure_dirs
foreach_mapping stop_port_forward
}
do_restart() {
validate_env
# Preflight: require Docker daemon and k3d (if applicable)
ensure_docker_running
ensure_k3d_ready_if_applicable
foreach_mapping stop_port_forward
foreach_mapping start_port_forward
}
do_status() {
validate_env
# Non-fatal awareness messages
if docker_is_running; then
log "[OK] Docker daemon is running"
else
if have docker; then
log "[WARN] Docker is not running"
else
log "[WARN] Docker CLI not found"
fi
fi
local _k3d_name
if _k3d_name="$(detect_k3d_context)"; then
if have k3d; then
if k3d_cluster_is_running "$_k3d_name"; then
log "[OK] k3d cluster '$_k3d_name' is running"
else
log "[WARN] k3d cluster '$_k3d_name' is not running"
fi
else
log "[WARN] k3d not installed but context suggests k3d (cluster='$_k3d_name')"
fi
fi
log "== Context / Cluster =="
"$KUBECTL" config current-context 2>/dev/null | sed 's/^/ context: /' || true
"$KUBECTL" cluster-info 2>/dev/null | sed 's/^/ /' || true
log ""
log "== Port-forward processes =="
foreach_mapping status_one
log ""
log "== Quick k3d awareness checks (best-effort) =="
# If user is on k3d, current-context often includes k3d-... but not guaranteed.
# Show nodes + a few namespaces/services related to mappings (best effort).
"$KUBECTL" get nodes -o wide 2>/dev/null | sed 's/^/ /' || true
log ""
"$KUBECTL" get ns 2>/dev/null | sed 's/^/ /' || true
log ""
# For each mapping, try to show target existence
log "== Target existence (best-effort) =="
_target_check() {
local id="$1" ns="$2" target="$3" address="$4" hostPort="$5" servicePort="$6" protocol="$7" description="$8"
# kubectl get <target> -n <ns>
# If target is like "svc/name", "deploy/name", etc.
if "$KUBECTL" get -n "$ns" "$target" >/dev/null 2>&1; then
printf 'OK %-12s %s (ns=%s)\n' "$id" "$target" "$ns"
else
printf 'MISSING %-12s %s (ns=%s)\n' "$id" "$target" "$ns"
fi
}
foreach_mapping _target_check
}
# ---- arg parsing ----
ACTION=""
while [ $# -gt 0 ]; do
case "$1" in
start|stop|restart|status)
if [ -z "$ACTION" ]; then
ACTION="$1"
else
TARGET_ID="$1"
fi
shift
;;
-v|--verbose)
VERBOSE=1
shift
;;
-c)
shift
[ $# -gt 0 ] || { err "-c requires a file path"; usage; exit 2; }
CONFIG_FILE="$1"
shift
;;
--config-file=*)
CONFIG_FILE="${1#*=}"
shift
;;
-h|--help)
usage
exit 0
;;
*)
if [ -z "$ACTION" ]; then
err "Unknown arg: $1"
usage
exit 2
fi
TARGET_ID="$1"
shift
;;
esac
done
[ -n "$ACTION" ] || { usage; exit 2; }
case "$ACTION" in
start) do_start ;;
stop) do_stop ;;
restart) do_restart ;;
status) do_status ;;
esac

View File

@ -1,330 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# init_prole-db.sh
# Purpose:
# - Manage prole-db CloudNative-PG cluster operations (deploy, start, stop, etc.)
# Initialize SCRIPT_DIR
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ACTION=${1:-}
VERSION=${2:-latest}
# Load env
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
set --
source "$PROLE_HOME/env.sh"
elif [[ -f "$HOME/.prole/env.sh" ]]; then
set --
source "$HOME/.prole/env.sh"
fi
CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db}
NAMESPACE=${NAMESPACE:-prole}
CNPG_MANIFEST="$SCRIPT_DIR/../k8s/prole/prole-db.yaml"
ensure_tools() {
for t in kubectl jq; do
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
done
}
get_latest_image() {
local version_file="$SCRIPT_DIR/../conf/postgresql/.version"
if [[ -f "$version_file" ]]; then
echo "prole-db:$(cat "$version_file" | tr -d '[:space:]')"
else
echo "prole-db:17.7-033"
fi
}
start() {
ensure_tools
echo "Checking dependencies..."
# 1. k3d is running
if ! command -v k3d >/dev/null || ! k3d cluster list >/dev/null 2>&1; then
echo "ERROR: k3d is not running or not installed." >&2
exit 1
fi
# 2. cnpg operator is loaded
if ! kubectl get deployment -n cnpg-system cnpg-controller-manager >/dev/null 2>&1; then
echo "ERROR: CloudNative-PG operator is not loaded." >&2
exit 1
fi
# 3. openbao is configured
if ! kubectl get statefulset openbao -n "$NAMESPACE" >/dev/null 2>&1 && ! kubectl get deployment openbao -n "$NAMESPACE" >/dev/null 2>&1; then
echo "ERROR: OpenBao is not deployed." >&2
exit 1
fi
# Compare latest image with deployed
local latest_image
latest_image=$(get_latest_image)
local current_image
current_image=$(kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.imageName}' 2>/dev/null || echo "")
if [[ "$current_image" != "$latest_image" ]]; then
echo "Updating cluster image from '$current_image' to '$latest_image'..."
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{\"spec\": {\"imageName\": \"$latest_image\"}}"
# Force a rollout to ensure the new image is pulled even if it was just a tag update (though we use unique tags)
kubectl cnpg restart "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" || true
else
echo "Cluster is already using the latest image: $latest_image"
fi
echo "Starting prole-db cluster (ensuring manifest is applied)..."
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST"
}
stop() {
ensure_tools
echo "Stopping prole-db cluster $CNPG_CLUSTER_NAME..."
# Identify instances
local instances
instances=$(kubectl get pods -n "$NAMESPACE" -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" -o jsonpath='{.items[*].metadata.name}')
if [[ -z "$instances" ]]; then
echo "No instances found for cluster $CNPG_CLUSTER_NAME."
return
fi
# Determine replicas and primary
local primary
primary=$(kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" -o jsonpath='{.status.currentPrimary}')
echo "Primary: $primary"
# Shutdown replicas first
for pod in $instances; do
if [[ "$pod" != "$primary" ]]; then
echo "Shutting down replica $pod..."
kubectl exec -n "$NAMESPACE" "$pod" -c postgres -- psql -U postgres -c "CHECKPOINT;" || true
fi
done
# Shutdown primary last
if [[ -n "$primary" ]]; then
echo "Shutting down primary $primary..."
kubectl exec -n "$NAMESPACE" "$primary" -c postgres -- psql -U postgres -c "CHECKPOINT;" || true
fi
echo "Deleting cluster resource..."
kubectl delete -f "$CNPG_MANIFEST" --ignore-not-found
}
restart() {
ensure_tools
echo "Restarting prole-db cluster..."
kubectl cnpg restart "$CNPG_CLUSTER_NAME" -n "$NAMESPACE"
}
deploy() {
ensure_tools
local image
if [[ "$VERSION" == "latest" ]]; then
image=$(get_latest_image)
else
image="prole-db:$VERSION"
fi
echo "Deploying $image to cluster $CNPG_CLUSTER_NAME..."
# If cluster doesn't exist, use init_cloudnative_pg.sh create first
if ! kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" >/dev/null 2>&1; then
echo "Cluster not found. Running init_cloudnative_pg.sh create..."
bash "$SCRIPT_DIR/init_cloudnative_pg.sh" create
fi
# Ensure image is updated if manifest has an older version
# First, apply the manifest to ensure the cluster exists/is updated
echo "Applying manifest $CNPG_MANIFEST..."
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST"
# Then force the specific image version via patch if different
local current_image
current_image=$(kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.imageName}' 2>/dev/null || echo "")
if [[ "$current_image" != "$image" ]]; then
echo "Patching cluster to use image '$image' (was '$current_image')..."
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{\"spec\": {\"imageName\": \"$image\"}}"
# Force a rollout
kubectl cnpg restart "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" || true
fi
}
rollback() {
ensure_tools
echo "Rollback: Updating to previous image (manually specified version or fallback)..."
if [[ "$VERSION" == "latest" ]]; then
echo "Please specify a version to rollback to. Usage: $0 rollback <version>"
exit 1
fi
deploy
}
rollout() {
ensure_tools
echo "Starting manual recreate rollout for $CNPG_CLUSTER_NAME in $NAMESPACE..."
# 1. Monitor status
echo "Current status:"
kubectl cnpg status "$CNPG_CLUSTER_NAME" -n "$NAMESPACE"
# 2. Identify the primary instance
local primary
primary=$(kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" -o jsonpath='{.status.currentPrimary}')
if [[ -z "$primary" ]]; then
echo "ERROR: Could not identify primary instance." >&2
exit 1
fi
echo "Primary instance: $primary"
# 3. Identify all pod instances
local instances
instances=$(kubectl get pods -n "$NAMESPACE" -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" -o jsonpath='{.items[*].metadata.name}')
# 4. Recreate non-primary instances one at a time
for pod in $instances; do
if [[ "$pod" != "$primary" ]]; then
echo "Recreating non-primary pod: $pod..."
kubectl delete pod -n "$NAMESPACE" "$pod"
echo "Waiting for $pod to be recreated and healthy..."
while true; do
if kubectl get pod -n "$NAMESPACE" "$pod" >/dev/null 2>&1; then
local status
status=$(kubectl get pod -n "$NAMESPACE" "$pod" -o jsonpath='{.status.phase}')
local ready
ready=$(kubectl get pod -n "$NAMESPACE" "$pod" -o jsonpath='{.status.containerStatuses[0].ready}')
if [[ "$status" == "Running" && "$ready" == "true" ]]; then
echo "Pod $pod is active and healthy."
break
fi
fi
echo -n "."
sleep 5
done
echo ""
# Additional wait for CNPG to recognize it as joined and healthy
echo "Waiting for CNPG cluster to stabilize..."
sleep 10
kubectl cnpg status "$CNPG_CLUSTER_NAME" -n "$NAMESPACE"
fi
done
# 5. When the last primary instance needs to be recreated, promote a new primary
echo "Promoting a new primary to recreate the old primary $primary..."
# We promote one of the newly recreated instances.
# kubectl cnpg promote <cluster> <instance>
local new_primary=""
for pod in $instances; do
if [[ "$pod" != "$primary" ]]; then
new_primary="$pod"
break
fi
done
if [[ -z "$new_primary" ]]; then
echo "ERROR: No candidate for new primary found." >&2
exit 1
fi
echo "Promoting $new_primary..."
kubectl cnpg promote "$CNPG_CLUSTER_NAME" "$new_primary" -n "$NAMESPACE" || {
echo "Promotion failed, but continuing rollout (CNPG might have already promoted it)..."
}
echo "Waiting for $new_primary to become primary..."
while true; do
local current_primary
current_primary=$(kubectl get cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" -o jsonpath='{.status.currentPrimary}')
if [[ "$current_primary" == "$new_primary" ]]; then
echo "$new_primary is now the primary."
break
fi
echo -n "."
sleep 5
done
echo ""
# 6. Rebuild the last pod (the old primary) to make the cluster consistent
echo "Recreating the old primary pod: $primary..."
kubectl delete pod -n "$NAMESPACE" "$primary"
echo "Waiting for $primary to be recreated and healthy..."
while true; do
if kubectl get pod -n "$NAMESPACE" "$primary" >/dev/null 2>&1; then
local status
status=$(kubectl get pod -n "$NAMESPACE" "$primary" -o jsonpath='{.status.phase}')
local ready
ready=$(kubectl get pod -n "$NAMESPACE" "$primary" -o jsonpath='{.status.containerStatuses[0].ready}')
if [[ "$status" == "Running" && "$ready" == "true" ]]; then
echo "Pod $primary is active and healthy."
break
fi
fi
echo -n "."
sleep 5
done
echo ""
echo "Rollout complete. Final cluster status:"
kubectl cnpg status "$CNPG_CLUSTER_NAME" -n "$NAMESPACE"
}
backup() {
echo "Backup - tbd, when we configure s3 or other block store"
}
reset() {
ensure_tools
echo "Resetting prole-db cluster..."
echo "Removing cnpg instances and pods..."
kubectl delete cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --ignore-not-found
# Wait for deletion
kubectl wait --for=delete cluster/"$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --timeout=60s || true
echo "Reinstantiating..."
deploy
}
case "$ACTION" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
deploy)
deploy
;;
rollback)
rollback
;;
rollout)
rollout
;;
backup)
backup
;;
reset)
reset
;;
*)
echo "Usage: $0 {start|stop|restart|deploy|rollback|backup|reset} [version]" >&2
exit 2
;;
esac

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -21,6 +21,12 @@
<webroots>
<root url="file://$MODULE_DIR$/web" relative="/" />
</webroots>
<sourceRoots>
<root url="file://$MODULE_DIR$/src/main/java" />
<root url="file://$MODULE_DIR$/src/main/resources" />
<root url="file://$MODULE_DIR$/target/generated-sources/annotations" />
<root url="file://$MODULE_DIR$/src" />
</sourceRoots>
</configuration>
</facet>
</component>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
</web-app>

View File

@ -1,131 +0,0 @@
# Prole Shop - Spring WebMVC Shopping Application
A comprehensive Spring Boot shopping application that supports both product sales and service bookings with calendar scheduling.
## Features
- **Product Catalog**: Browse and purchase physical products
- **Service Booking**: Book professional services with calendar scheduling
- **Shopping Cart**: Session-based shopping cart for products and services
- **Checkout Process**: Complete order processing with customer information
- **Shipping Estimation**: Automatic shipping cost calculation based on weight and order value
- **Calendar Integration**: View and book available time slots for services
- **Order Management**: Track orders with status and payment information
## Technology Stack
- **Spring Boot 3.1.0**: Main framework
- **Spring WebMVC**: Web layer
- **Spring Data JPA**: Data access layer
- **Thymeleaf**: Template engine
- **PostgreSQL**: Database (prole-db service)
- **Bootstrap 5**: UI framework
## Database Configuration
The application connects to the `prole-db` PostgreSQL database service defined in `cloudnative-pg-prole0-db.yaml`.
Connection details:
- **Host**: prole-db-rw
- **Port**: 5432
- **Database**: prole-db
- **User**: prole
- **Password**: Fr0b0zzg0bl!n!
## Project Structure
```
www/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── org/prole/shop/
│ │ │ ├── ShopApplication.java
│ │ │ ├── controller/ # MVC Controllers
│ │ │ ├── model/ # JPA Entities
│ │ │ ├── repository/ # Data Repositories
│ │ │ └── service/ # Business Logic
│ │ └── resources/
│ │ ├── application.properties
│ │ ├── schema.sql # Database schema
│ │ ├── data.sql # Sample data
│ │ ├── templates/ # Thymeleaf templates
│ │ └── static/ # Static resources
│ └── test/
└── pom.xml
```
## Running the Application
1. Ensure the `prole-db` database service is running and accessible
2. Build the project:
```bash
cd www
mvn clean install
```
3. Run the application:
```bash
mvn spring-boot:run
```
4. Access the application at: http://localhost:8080
## Application Endpoints
- `/` - Home page with featured products and services
- `/products` - List all products
- `/products/{id}` - Product detail page
- `/services` - List all services
- `/services/{id}` - Service detail page with calendar booking
- `/cart` - Shopping cart view
- `/checkout` - Checkout form
- `/checkout/success` - Order confirmation
## Database Schema
The application uses the following main entities:
- **Product**: Physical products for sale
- **Service**: Professional services available for booking
- **ScheduleSlot**: Available time slots for services
- **CartItem**: Items in the shopping cart (session-based)
- **Order**: Customer orders
- **OrderItem**: Items within an order
## Payment Processing
Payment processing is currently a placeholder. The `OrderService.processPayment()` method simulates payment processing. Integration with an external payment processor (Stripe, PayPal, etc.) should be implemented in this method.
## Shipping Calculation
Shipping costs are calculated based on:
- Base shipping cost: $10.00
- Cost per kg: $2.00
- Free shipping threshold: $100.00
These values can be configured in `ShippingService.java`.
## Sample Data
The application includes sample data for development/testing:
- 6 sample products
- 6 sample services
- Sample schedule slots for services
## Development Notes
- The application uses session-based cart management
- Database schema is auto-created on startup (spring.jpa.hibernate.ddl-auto=update)
- Sample data is loaded from `data.sql` on startup
- Thymeleaf templates use Bootstrap 5 for styling
## Future Enhancements
- User authentication and accounts
- Order history and tracking
- Email notifications
- Payment processor integration (Stripe, PayPal, etc.)
- Advanced calendar features (recurring appointments, availability management)
- Product reviews and ratings
- Inventory management
- Admin dashboard

Binary file not shown.

Before

Width:  |  Height:  |  Size: 599 B

View File

@ -1,78 +0,0 @@
<!DOCTYPE html>
<html class="img-no-display"><head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
<title>prole.org</title>
<style>
html {
height: 100%;
overflow: auto;
padding: 0;
margin: 0;
}
body {
height: 100%;
padding: 0;
margin: 0;
}
div#outer {
display: table;
height: 100%;
width: 100%;
}
div#inner {
display: table-cell;
text-align: center;
vertical-align: middle;
}
div#container {
display: flex;
flex-direction: column;
justify-content: center;
min-width: 800px;
min-height: 580px;
}
img {
width: 500px;
height: 330px;
margin: 30px 0;
}
p#header {
font-family: Roboto-Medium;
font-size: 28px;
color: #323C46;
text-align: center;
line-height: 36px;
margin-top: 0;
margin-bottom: 12px;
}
p#paragraph {
font-family: Roboto-Regular;
font-size: 13px;
color: #323C46;
text-align: center;
line-height: 20px;
margin: 0 auto;
}
</style>
<link href="../help.css" type="text/css" rel="stylesheet" />
<link href="../scrollbar/flexcroll.css" type="text/css" rel="stylesheet" />
<script type="text/javascript" src="../scrollbar/flexcroll.js"></script>
<script type="text/javascript" src="../scrollbar/initFlexcroll.js"></script>
</head>
<body>
<div id="outer">
<div id="inner">
<div id="container">
<div>
<p align=center>
<img src="images/prole-type.gif" border=0>
</p>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,762 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prole Service Dependencies Installer</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #1a1a1a;
color: #e0e0e0;
overflow: hidden;
height: 100vh;
position: relative;
}
#background-image {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('images/prole-type.gif');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
opacity: 0.15;
z-index: 0;
}
.installer-container {
position: relative;
z-index: 1;
height: 100vh;
display: flex;
flex-direction: column;
background: rgba(26, 26, 26, 0.85);
backdrop-filter: blur(10px);
}
.installer-header {
padding: 30px;
text-align: center;
border-bottom: 2px solid #333;
}
.installer-header h1 {
font-size: 32px;
color: #4a9eff;
margin-bottom: 10px;
}
.installer-header p {
color: #aaa;
font-size: 14px;
}
.installer-content {
flex: 1;
padding: 40px;
overflow-y: auto;
}
.step {
display: none;
max-width: 900px;
margin: 0 auto;
}
.step.active {
display: block;
}
.checklist-item {
background: rgba(255, 255, 255, 0.05);
border: 1px solid #333;
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
display: flex;
align-items: center;
justify-content: space-between;
}
.checklist-item-content {
flex: 1;
}
.checklist-item-title {
font-size: 18px;
font-weight: 600;
color: #fff;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 10px;
}
.checklist-item-description {
font-size: 14px;
color: #aaa;
margin-top: 5px;
}
.sub-checklist {
margin-top: 15px;
margin-left: 30px;
padding-left: 20px;
border-left: 2px solid #444;
}
.sub-checklist-item {
background: rgba(255, 255, 255, 0.03);
border: 1px solid #2a2a2a;
border-radius: 6px;
padding: 12px;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
.status-indicator {
display: flex;
align-items: center;
gap: 15px;
}
.checkmark {
width: 24px;
height: 24px;
border-radius: 50%;
border: 2px solid #4a9eff;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.checkmark.installed {
background: #4a9eff;
border-color: #4a9eff;
}
.checkmark.installed::after {
content: '✓';
color: white;
font-size: 16px;
font-weight: bold;
}
.checkmark.not-installed {
background: transparent;
border-color: #666;
}
.checkmark.not-installed::after {
content: '✗';
color: #666;
font-size: 16px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
text-decoration: none;
display: inline-block;
}
.btn-primary {
background: #4a9eff;
color: white;
}
.btn-primary:hover {
background: #3a8eef;
transform: translateY(-2px);
}
.btn-secondary {
background: #555;
color: white;
}
.btn-secondary:hover {
background: #666;
}
.btn-download {
background: #28a745;
color: white;
}
.btn-download:hover {
background: #218838;
}
.btn-update {
background: #ffc107;
color: #1a1a1a;
}
.btn-update:hover {
background: #e0a800;
}
.command-display {
background: #0a0a0a;
border: 1px solid #333;
border-radius: 6px;
padding: 20px;
margin: 20px 0;
font-family: 'Courier New', monospace;
font-size: 14px;
color: #4a9eff;
overflow-x: auto;
}
.command-display code {
color: #4a9eff;
white-space: pre-wrap;
}
.installer-footer {
padding: 20px 40px;
border-top: 2px solid #333;
display: flex;
justify-content: space-between;
align-items: center;
}
.btn-large {
padding: 12px 30px;
font-size: 16px;
}
.terminal-window {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
max-width: 800px;
height: 500px;
background: rgba(10, 10, 10, 0.95);
border: 2px solid #4a9eff;
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
z-index: 1000;
display: none;
flex-direction: column;
backdrop-filter: blur(10px);
}
.terminal-window.active {
display: flex;
}
.terminal-window::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('images/prole-type.gif');
background-size: cover;
background-position: center;
opacity: 0.2;
z-index: -1;
border-radius: 6px;
}
.terminal-header {
padding: 15px;
background: rgba(74, 158, 255, 0.2);
border-bottom: 1px solid #4a9eff;
display: flex;
justify-content: space-between;
align-items: center;
}
.terminal-title {
color: #4a9eff;
font-weight: 600;
}
.terminal-close {
background: #ff4444;
color: white;
border: none;
width: 24px;
height: 24px;
border-radius: 50%;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.terminal-body {
flex: 1;
padding: 20px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 14px;
color: #4a9eff;
}
.terminal-prompt {
color: #4a9eff;
margin-bottom: 10px;
}
.terminal-input {
background: transparent;
border: none;
color: #4a9eff;
font-family: 'Courier New', monospace;
font-size: 14px;
width: 100%;
outline: none;
}
.terminal-output {
color: #aaa;
margin-top: 10px;
}
.cluster-status {
background: #0a0a0a;
border: 1px solid #333;
border-radius: 6px;
padding: 20px;
margin: 20px 0;
font-family: 'Courier New', monospace;
font-size: 14px;
color: #4a9eff;
min-height: 200px;
max-height: 400px;
overflow-y: auto;
}
.cluster-status pre {
margin: 0;
color: #4a9eff;
}
.status-text {
color: #aaa;
font-style: italic;
}
</style>
</head>
<body>
<div id="background-image"></div>
<div class="installer-container">
<div class="installer-header">
<h1>Prole Service Dependencies Installer</h1>
<p>Install and configure required dependencies for Prole services</p>
</div>
<div class="installer-content">
<!-- Step 1: Dependency Installation -->
<div class="step active" id="step1">
<h2 style="margin-bottom: 30px; color: #4a9eff;">Step 1: Install Dependencies</h2>
<!-- Docker Check -->
<div class="checklist-item">
<div class="checklist-item-content">
<div class="checklist-item-title">
<span>Docker</span>
</div>
<div class="checklist-item-description">
Container platform required for running Prole services
</div>
</div>
<div class="status-indicator">
<div class="checkmark not-installed" id="docker-check"></div>
<a href="https://www.docker.com/products/docker-desktop" target="_blank" class="btn btn-download">Click here to download</a>
</div>
</div>
<!-- Homebrew Check -->
<div class="checklist-item">
<div class="checklist-item-content">
<div class="checklist-item-title">
<span>Homebrew</span>
</div>
<div class="checklist-item-description">
Package manager for macOS (required for k3d installation)
</div>
<div class="sub-checklist">
<div class="sub-checklist-item">
<div class="checklist-item-content">
<div class="checklist-item-title" style="font-size: 16px;">
<span>Homebrew Installed & Current</span>
</div>
</div>
<div class="status-indicator">
<div class="checkmark not-installed" id="homebrew-check"></div>
<button class="btn btn-update" id="update-homebrew-btn">Update Homebrew</button>
</div>
</div>
</div>
</div>
</div>
<!-- k3d Check -->
<div class="checklist-item">
<div class="checklist-item-content">
<div class="checklist-item-title">
<span>k3d</span>
</div>
<div class="checklist-item-description">
Lightweight wrapper to run k3s in Docker (installed via Homebrew)
</div>
</div>
<div class="status-indicator">
<div class="checkmark not-installed" id="k3d-check"></div>
<a href="https://k3d.io/" target="_blank" class="btn btn-download">Click here to download</a>
</div>
</div>
<!-- Command Display -->
<div class="command-display">
<strong style="color: #fff; margin-bottom: 10px; display: block;">Commands to execute:</strong>
<code id="install-commands"># Commands will appear here after checking dependencies</code>
</div>
<div style="text-align: center; margin-top: 30px;">
<button class="btn btn-primary btn-large" id="install-btn">Install</button>
</div>
</div>
<!-- Step 2: Create Home Cluster -->
<div class="step" id="step2">
<h2 style="margin-bottom: 30px; color: #4a9eff;">Step 2: Create Home Cluster</h2>
<!-- Docker Running Check -->
<div class="checklist-item">
<div class="checklist-item-content">
<div class="checklist-item-title">
<span>Docker Installed and Running</span>
</div>
<div class="checklist-item-description">
Verify Docker is installed and the daemon is running
</div>
</div>
<div class="status-indicator">
<div class="checkmark not-installed" id="docker-running-check"></div>
</div>
</div>
<!-- k3d Ready Check -->
<div class="checklist-item">
<div class="checklist-item-content">
<div class="checklist-item-title">
<span>k3d Command and Dependencies Ready</span>
</div>
<div class="checklist-item-description">
Verify k3d and all related dependencies are installed and ready
</div>
</div>
<div class="status-indicator">
<div class="checkmark not-installed" id="k3d-ready-check"></div>
</div>
</div>
<!-- k3d Cluster List -->
<div class="cluster-status">
<strong style="color: #fff; margin-bottom: 10px; display: block;">k3d cluster list output:</strong>
<pre id="cluster-list-output" class="status-text">Running k3d cluster list...</pre>
</div>
</div>
</div>
<div class="installer-footer">
<button class="btn btn-secondary" id="prev-btn" style="display: none;">Previous</button>
<button class="btn btn-primary btn-large" id="next-btn">Next</button>
</div>
</div>
<!-- Terminal Window -->
<div class="terminal-window" id="terminal-window">
<div class="terminal-header">
<div class="terminal-title">Prole Terminal</div>
<button class="terminal-close" id="terminal-close">×</button>
</div>
<div class="terminal-body" id="terminal-body">
<div class="terminal-prompt">$ what is my name?</div>
<div class="terminal-output" id="terminal-output"></div>
<div class="terminal-prompt" id="terminal-input-line" style="display: none;">
$ <input type="text" class="terminal-input" id="terminal-input" autocomplete="off">
</div>
</div>
</div>
<script>
let currentStep = 1;
let userName = '';
let terminalActive = false;
// Check dependencies on load
async function checkDependencies() {
// Simulate checking dependencies
// In a real implementation, this would call a backend API
// Check Docker
try {
const dockerCheck = await checkCommand('docker --version');
updateCheckmark('docker-check', dockerCheck);
} catch (e) {
updateCheckmark('docker-check', false);
}
// Check Homebrew
try {
const brewCheck = await checkCommand('brew --version');
updateCheckmark('homebrew-check', brewCheck);
} catch (e) {
updateCheckmark('homebrew-check', false);
}
// Check k3d
try {
const k3dCheck = await checkCommand('k3d --version');
updateCheckmark('k3d-check', k3dCheck);
} catch (e) {
updateCheckmark('k3d-check', false);
}
updateInstallCommands();
}
// Simulate command checking (in real app, this would use a backend)
async function checkCommand(command) {
// This is a simulation - in a real app, you'd call a backend API
// For now, we'll simulate based on common scenarios
return new Promise((resolve) => {
setTimeout(() => {
// Simulate: docker and brew are often installed, k3d less so
if (command.includes('docker')) {
resolve(Math.random() > 0.3); // 70% chance installed
} else if (command.includes('brew')) {
resolve(Math.random() > 0.2); // 80% chance installed
} else if (command.includes('k3d')) {
resolve(Math.random() > 0.7); // 30% chance installed
} else {
resolve(false);
}
}, 500);
});
}
function updateCheckmark(id, installed) {
const checkmark = document.getElementById(id);
if (installed) {
checkmark.classList.remove('not-installed');
checkmark.classList.add('installed');
} else {
checkmark.classList.remove('installed');
checkmark.classList.add('not-installed');
}
}
function updateInstallCommands() {
const dockerInstalled = document.getElementById('docker-check').classList.contains('installed');
const brewInstalled = document.getElementById('homebrew-check').classList.contains('installed');
const k3dInstalled = document.getElementById('k3d-check').classList.contains('installed');
let commands = [];
if (!brewInstalled) {
commands.push('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"');
} else {
commands.push('brew update');
commands.push('brew upgrade');
}
if (!k3dInstalled && brewInstalled) {
commands.push('brew install k3d');
}
const commandsDisplay = document.getElementById('install-commands');
if (commands.length > 0) {
commandsDisplay.textContent = commands.join('\n');
} else {
commandsDisplay.textContent = '# All dependencies are already installed!';
}
}
// Update Homebrew button
document.getElementById('update-homebrew-btn').addEventListener('click', () => {
const commandsDisplay = document.getElementById('install-commands');
commandsDisplay.textContent = 'brew update\nbrew upgrade';
alert('Click the Install button to update Homebrew');
});
// Install button
document.getElementById('install-btn').addEventListener('click', () => {
const commands = document.getElementById('install-commands').textContent;
if (commands.includes('All dependencies')) {
alert('All dependencies are already installed!');
return;
}
// Show commands that will be executed
const confirmed = confirm(`The following commands will be executed:\n\n${commands}\n\nProceed?`);
if (confirmed) {
// In a real app, this would execute the commands via a backend
alert('Installation commands executed! (This is a simulation - in production, commands would run via backend)');
// Simulate installation success
setTimeout(() => {
checkDependencies();
}, 2000);
}
});
// Next button
document.getElementById('next-btn').addEventListener('click', () => {
if (currentStep === 1) {
// Move to step 2
document.getElementById('step1').classList.remove('active');
document.getElementById('step2').classList.add('active');
document.getElementById('prev-btn').style.display = 'block';
currentStep = 2;
// Check cluster status
checkClusterStatus();
} else if (currentStep === 2) {
// Open terminal window
openTerminal();
}
});
// Previous button
document.getElementById('prev-btn').addEventListener('click', () => {
if (currentStep === 2) {
document.getElementById('step2').classList.remove('active');
document.getElementById('step1').classList.add('active');
document.getElementById('prev-btn').style.display = 'none';
currentStep = 1;
}
});
// Check cluster status
async function checkClusterStatus() {
// Check Docker running
try {
const dockerRunning = await checkCommand('docker ps');
updateCheckmark('docker-running-check', dockerRunning);
} catch (e) {
updateCheckmark('docker-running-check', false);
}
// Check k3d ready
try {
const k3dReady = await checkCommand('k3d --version');
updateCheckmark('k3d-ready-check', k3dReady);
} catch (e) {
updateCheckmark('k3d-ready-check', false);
}
// Run k3d cluster list
const clusterOutput = document.getElementById('cluster-list-output');
clusterOutput.textContent = 'Running: k3d cluster list\n\n';
// Simulate k3d cluster list output
setTimeout(() => {
const output = `$ k3d cluster list
NAME CLUSTER SERVERS AGENTS LOADBALANCER
prole-data-cluster running 1 0 true
Clusters found: 1`;
clusterOutput.textContent = output;
}, 1000);
}
// Terminal functions
function openTerminal() {
const terminal = document.getElementById('terminal-window');
terminal.classList.add('active');
terminalActive = true;
// Start the prompt sequence
setTimeout(() => {
const inputLine = document.getElementById('terminal-input-line');
inputLine.style.display = 'block';
document.getElementById('terminal-input').focus();
}, 500);
}
document.getElementById('terminal-close').addEventListener('click', () => {
document.getElementById('terminal-window').classList.remove('active');
terminalActive = false;
userName = '';
document.getElementById('terminal-output').innerHTML = '';
document.getElementById('terminal-input-line').style.display = 'none';
document.getElementById('terminal-input').value = '';
});
document.getElementById('terminal-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const input = document.getElementById('terminal-input');
const value = input.value.trim();
const output = document.getElementById('terminal-output');
if (!userName) {
// First question: "what is my name?"
userName = value || 'Prole';
output.innerHTML = `<div style="color: #4a9eff;">$ ${value}</div>`;
output.innerHTML += `<div style="color: #aaa; margin-top: 10px;">Hi, I'm ${userName}. What should I call you?</div>`;
input.value = '';
// Update prompt
setTimeout(() => {
const promptLine = document.querySelector('.terminal-prompt');
if (promptLine) {
promptLine.textContent = `$ `;
}
}, 100);
} else {
// Second question: "What should I call you?"
const userResponse = value || 'User';
output.innerHTML += `<div style="color: #4a9eff;">$ ${value}</div>`;
output.innerHTML += `<div style="color: #aaa; margin-top: 10px;">Nice to meet you, ${userResponse}! I'm ${userName}.</div>`;
input.value = '';
// Close terminal after a moment
setTimeout(() => {
document.getElementById('terminal-window').classList.remove('active');
terminalActive = false;
}, 2000);
}
}
});
// Initialize
checkDependencies();
</script>
</body>
</html>

View File

@ -1,86 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/>
</parent>
<groupId>org.prole</groupId>
<artifactId>prole-shop</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>prole-shop</name>
<description>Spring WebMVC Shopping Application</description>
<properties>
<java.version>24</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring Batch for scheduled exports -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Apache Avro for serialization -->
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro</artifactId>
<version>1.11.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -1,104 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>prole.soundBlock - Sound Isolation Made Easy | sound.prole.org</title>
<link rel="stylesheet" href="style.css"> <!-- Link to your CSS file -->
</head>
<body>
<header>
<h1><a href="https://sound.prole.org">sound.prole.org</a></h1>
<nav>
<!-- Add navigation links here if needed -->
</nav>
</header>
<main>
<section id="product-details">
<div class="product-image">
<img src="placeholder-soundblock.jpg" alt="prole.soundBlock" width="400">
</div>
<div class="product-info">
<h2>prole.soundBlock - 12" x 12" Sound Isolation Block</h2>
<p class="price">$49.99 (Price varies by location - see details below)</p>
<p>
Tired of unwanted noise? The <strong>prole.soundBlock</strong> is a revolutionary sound isolation solution built on proven floating wall construction principles. We take the complexity out of soundproofing, delivering pre-built, interlocking 12" x 12" blocks that are easy to install and incredibly effective.
</p>
<p>
Each block is meticulously crafted to meet local building codes, utilizing the maximum density rockwool (or equivalent high-performance poly insulation) permitted by fire regulations in your area. We handle the research, ensuring your project is safe and compliant.
</p>
<h3>Key Features:</h3>
<ul>
<li><strong>Dimensions:</strong> 12" x 12"</li>
<li><strong>Construction:</strong> Robust 2x4 or 2x6 frame (choose option), 1/4" plywood layers, high-density rockwool/poly insulation, acoustic sealant, and finished with Homosote 440 for superior performance. Mitered corners, glued & tacked construction.</li>
<li><strong>Customizable:</strong> We ensure compliance with *your* local fire codes and building regulations.</li>
<li><strong>Easy Installation:</strong> Interlocking design simplifies installation - no specialized skills required.</li>
<li><strong>Superior Sound Isolation:</strong> Effectively reduces noise transmission across a wide frequency range.</li>
</ul>
<h3>Applications:</h3>
<ul>
<li><strong>Apartment Noise Management:</strong> Reduce noise bleed between apartments, creating a more peaceful living environment.</li>
<li><strong>Recording Studio Sound Isolation:</strong> Build a quiet and controlled recording space, minimizing external noise and reflections.</li>
<li><strong>Home Audio Bass Traps:</strong> Enhance your home theater experience by absorbing low-frequency sound waves and reducing unwanted vibrations.</li>
<li><strong>Sound Reinforcement Applications:</strong> Isolate sound sources in venues, churches, or performance spaces.</li>
</ul>
<div class="location-disclaimer">
<p><strong>Important:</strong> Pricing and material selection (rockwool vs. poly) are dependent on your location to ensure code compliance. Please enter your zip code below to receive a personalized quote.</p>
<form id="location-form">
<label for="zipcode">Zip Code:</label>
<input type="text" id="zipcode" name="zipcode" required>
<button type="submit">Get Quote</button>
</form>
</div>
<div class="add-to-cart">
<label for="quantity">Quantity:</label>
<input type="number" id="quantity" name="quantity" value="1" min="1">
<button id="add-to-cart-button">Add to Cart</button>
</div>
</div>
</section>
<section id="professional-services">
<h2>Professional Installation & Consultation</h2>
<p>
Need help designing and installing your sound isolation system? Our team at <strong>Kifuthu LLC</strong> offers expert consultation and professional installation services. <a href="mailto:info@kifuthu.com">Contact us</a> for a free estimate.
</p>
</section>
</main>
<footer>
<p>&copy; 2024 sound.prole.org</p>
</footer>
<script>
// JavaScript for Add to Cart functionality (example)
document.getElementById('add-to-cart-button').addEventListener('click', function() {
// Get quantity from input
const quantity = document.getElementById('quantity').value;
// Here you would integrate with your shopping cart application.
// This is a placeholder - you'll need to replace this with your actual cart integration code.
alert(`Added ${quantity} prole.soundBlock to cart! (This is a placeholder)`);
});
//JavaScript for zip code form submission
document.getElementById('location-form').addEventListener('submit', function(event) {
event.preventDefault();
const zipcode = document.getElementById('zipcode').value;
alert(`Submitting zip code ${zipcode} to calculate price and material options. (This is a placeholder)`);
// In a real application, you would send this zip code to your backend to determine
// compliance with local building codes and calculate the correct price.
});
</script>
</body>
</html>

View File

@ -1,13 +0,0 @@
package org.prole.shop;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ShopApplication {
public static void main(String[] args) {
SpringApplication.run(ShopApplication.class, args);
}
}

View File

@ -1,111 +0,0 @@
package org.prole.shop.batch;
import org.apache.avro.Schema;
import org.apache.avro.file.DataFileWriter;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericDatumWriter;
import org.apache.avro.generic.GenericRecord;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.ItemStream;
import org.springframework.batch.item.ItemStreamException;
import org.springframework.batch.item.ItemWriter;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
/**
* Generic Avro writer that writes rows (as Map<String,Object>) to a single Avro file per step execution.
* All values are stringified to avoid complex type conversions and ensure broad compatibility for archiving.
*/
public class AvroMapItemWriter implements ItemWriter<Map<String, Object>>, ItemStream {
private final String tableName;
private final String basePath;
private final List<String> columns;
private DataFileWriter<GenericRecord> dataFileWriter;
private Schema schema;
public AvroMapItemWriter(String tableName, String basePath, List<String> columns) {
this.tableName = tableName;
this.basePath = basePath;
this.columns = columns;
}
@Override
public void open(ExecutionContext executionContext) throws ItemStreamException {
try {
long ts = executionContext.containsKey("exportTimestamp")
? executionContext.getLong("exportTimestamp")
: System.currentTimeMillis();
LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneId.systemDefault());
// Build directory structure base/YYYY/MM/DD
String year = DateTimeFormatter.ofPattern("yyyy").format(dateTime);
String month = DateTimeFormatter.ofPattern("MM").format(dateTime);
String day = DateTimeFormatter.ofPattern("dd").format(dateTime);
Path dir = Path.of(basePath, year, month, day);
Files.createDirectories(dir);
String timestampPart = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(dateTime);
String fileName = tableName + "-" + timestampPart + ".avro";
Path filePath = dir.resolve(fileName);
// Build schema: each column is a nullable string
this.schema = buildSchema(tableName, columns);
this.dataFileWriter = new DataFileWriter<>(new GenericDatumWriter<>(schema));
this.dataFileWriter.create(schema, new File(filePath.toString()));
} catch (IOException e) {
throw new ItemStreamException("Failed to open Avro writer", e);
}
}
@Override
public void update(ExecutionContext executionContext) throws ItemStreamException {
// Nothing to update periodically
}
@Override
public void close() throws ItemStreamException {
if (dataFileWriter != null) {
try {
dataFileWriter.close();
} catch (IOException e) {
throw new ItemStreamException("Failed to close Avro writer", e);
}
}
}
@Override
public void write(List<? extends Map<String, Object>> items) throws Exception {
for (Map<String, Object> row : items) {
GenericRecord record = new GenericData.Record(schema);
for (String col : columns) {
Object val = row.get(col);
record.put(col, val == null ? null : String.valueOf(val));
}
dataFileWriter.append(record);
}
}
private static Schema buildSchema(String table, List<String> columns) {
Schema record = Schema.createRecord(table, "Archive export for table " + table, "org.prole.shop.archive", false);
List<Schema.Field> fields = new java.util.ArrayList<>();
for (String col : columns) {
Schema nullableString = Schema.createUnion(List.of(Schema.create(Schema.Type.NULL), Schema.create(Schema.Type.STRING)));
fields.add(new Schema.Field(col, nullableString, null, Schema.NULL_VALUE));
}
record.setFields(fields);
return record;
}
}

View File

@ -1,159 +0,0 @@
package org.prole.shop.batch;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.listener.StepExecutionListenerSupport;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.database.JdbcCursorItemReader;
import org.springframework.batch.item.database.builder.JdbcCursorItemReaderBuilder;
import org.springframework.batch.item.database.support.PostgresPagingQueryProvider;
import org.springframework.batch.item.support.builder.CompositeItemWriterBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.jdbc.core.ColumnMapRowMapper;
import javax.sql.DataSource;
import java.time.Clock;
import java.util.List;
import java.util.Map;
@Configuration
@EnableBatchProcessing
@EnableScheduling
public class BatchExportConfiguration {
@Value("${prole.archive.base-path:storage/archive}")
private String archiveBasePath;
@Bean
public Clock systemClock() {
return Clock.systemDefaultZone();
}
// --- Columns for each table ---
private static final List<String> PRODUCTS_COLS = List.of(
"id","name","description","price","stock_quantity","sku","image_url","active","weight","length","width","height","created_at","updated_at"
);
private static final List<String> SERVICES_COLS = List.of(
"id","name","description","price_per_hour","default_duration","image_url","active","provider_name","provider_email","provider_phone","created_at","updated_at"
);
private static final List<String> SLOTS_COLS = List.of(
"id","service_id","start_time","end_time","available","created_at","updated_at"
);
private static final List<String> CART_ITEMS_COLS = List.of(
"id","session_id","product_id","service_id","schedule_slot_id","quantity","unit_price","total_price","created_at","updated_at"
);
private static final List<String> ORDERS_COLS = List.of(
"id","order_number","subtotal","shipping_cost","tax","total","status","payment_status","customer_name","customer_email","customer_phone","shipping_address_line1","shipping_address_line2","shipping_city","shipping_state","shipping_postal_code","shipping_country","payment_processor_transaction_id","payment_method","created_at","updated_at","shipped_at","delivered_at"
);
private static final List<String> ORDER_ITEMS_COLS = List.of(
"id","order_id","product_id","service_id","schedule_slot_id","quantity","unit_price","total_price","item_name"
);
// --- ItemReaders per table using simple SELECT ---
private JdbcCursorItemReader<Map<String, Object>> readerFor(DataSource ds, String table, List<String> cols) {
String sql = "SELECT " + String.join(",", cols) + " FROM " + table + " ORDER BY 1";
return new JdbcCursorItemReaderBuilder<Map<String, Object>>()
.name("reader_" + table)
.dataSource(ds)
.sql(sql)
.rowMapper(new ColumnMapRowMapper())
.build();
}
private AvroMapItemWriter writerFor(String table, List<String> cols) {
return new AvroMapItemWriter(table, archiveBasePath, cols);
}
private Step stepFor(String table,
List<String> cols,
DataSource dataSource,
JobRepository jobRepository,
PlatformTransactionManager txManager) {
var reader = readerFor(dataSource, table, cols);
var writer = writerFor(table, cols);
return new StepBuilder("export_" + table, jobRepository)
.<Map<String, Object>, Map<String, Object>>chunk(500, txManager)
.reader(reader)
.writer(writer)
.listener(new TimestampPropagationListener())
.build();
}
@Bean
public Step exportProductsStep(DataSource ds, JobRepository jr, PlatformTransactionManager tx) {
return stepFor("products", PRODUCTS_COLS, ds, jr, tx);
}
@Bean
public Step exportServicesStep(DataSource ds, JobRepository jr, PlatformTransactionManager tx) {
return stepFor("services", SERVICES_COLS, ds, jr, tx);
}
@Bean
public Step exportScheduleSlotsStep(DataSource ds, JobRepository jr, PlatformTransactionManager tx) {
return stepFor("schedule_slots", SLOTS_COLS, ds, jr, tx);
}
@Bean
public Step exportCartItemsStep(DataSource ds, JobRepository jr, PlatformTransactionManager tx) {
return stepFor("cart_items", CART_ITEMS_COLS, ds, jr, tx);
}
@Bean
public Step exportOrdersStep(DataSource ds, JobRepository jr, PlatformTransactionManager tx) {
return stepFor("orders", ORDERS_COLS, ds, jr, tx);
}
@Bean
public Step exportOrderItemsStep(DataSource ds, JobRepository jr, PlatformTransactionManager tx) {
return stepFor("order_items", ORDER_ITEMS_COLS, ds, jr, tx);
}
@Bean
public Job hourlyArchiveJob(JobRepository jobRepository,
Step exportProductsStep,
Step exportServicesStep,
Step exportScheduleSlotsStep,
Step exportCartItemsStep,
Step exportOrdersStep,
Step exportOrderItemsStep) {
return new JobBuilder("hourlyArchiveJob", jobRepository)
.start(exportProductsStep)
.next(exportServicesStep)
.next(exportScheduleSlotsStep)
.next(exportCartItemsStep)
.next(exportOrdersStep)
.next(exportOrderItemsStep)
.build();
}
/**
* Listener to seed a single timestamp into each Step's execution context so
* all files share the same time suffix and directory for a given run.
*/
static class TimestampPropagationListener extends StepExecutionListenerSupport {
@Override
public void beforeStep(org.springframework.batch.core.StepExecution stepExecution) {
ExecutionContext ctx = stepExecution.getExecutionContext();
if (!ctx.containsKey("exportTimestamp")) {
long ts = stepExecution.getJobParameters().getLong("timestamp", System.currentTimeMillis());
ctx.putLong("exportTimestamp", ts);
}
}
}
}

View File

@ -1,39 +0,0 @@
package org.prole.shop.batch;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.Instant;
@Component
public class HourlyArchiveScheduler {
private final JobLauncher jobLauncher;
private final Job hourlyArchiveJob;
@Value("${prole.archive.enabled:true}")
private boolean archiveEnabled;
public HourlyArchiveScheduler(JobLauncher jobLauncher, Job hourlyArchiveJob) {
this.jobLauncher = jobLauncher;
this.hourlyArchiveJob = hourlyArchiveJob;
}
// Top of every hour
@Scheduled(cron = "0 0 * * * *")
public void runHourlyExport() throws Exception {
if (!archiveEnabled) return;
long ts = Instant.now().toEpochMilli();
JobParameters params = new JobParametersBuilder()
.addLong("timestamp", ts)
.toJobParameters();
jobLauncher.run(hourlyArchiveJob, params);
}
}

View File

@ -1,87 +0,0 @@
package org.prole.shop.controller;
import org.prole.shop.model.CartItem;
import org.prole.shop.service.CartService;
import org.prole.shop.service.ShippingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpSession;
import java.math.BigDecimal;
import java.util.List;
@Controller
@RequestMapping("/cart")
public class CartController {
@Autowired
private CartService cartService;
@Autowired
private ShippingService shippingService;
private String getSessionId(HttpSession session) {
String sessionId = (String) session.getAttribute("sessionId");
if (sessionId == null) {
sessionId = java.util.UUID.randomUUID().toString();
session.setAttribute("sessionId", sessionId);
}
return sessionId;
}
@GetMapping
public String viewCart(HttpSession session, Model model) {
String sessionId = getSessionId(session);
List<CartItem> cartItems = cartService.getCartItems(sessionId);
BigDecimal subtotal = cartService.getCartTotal(sessionId);
BigDecimal shippingCost = shippingService.calculateShippingCost(cartItems, subtotal);
BigDecimal tax = subtotal.add(shippingCost).multiply(new java.math.BigDecimal("0.08"));
BigDecimal total = subtotal.add(shippingCost).add(tax);
model.addAttribute("cartItems", cartItems);
model.addAttribute("subtotal", subtotal);
model.addAttribute("shippingCost", shippingCost);
model.addAttribute("tax", tax);
model.addAttribute("total", total);
model.addAttribute("estimatedDelivery", shippingService.estimateDeliveryDays(cartItems));
return "cart/view";
}
@PostMapping("/add/product/{productId}")
public String addProductToCart(
@PathVariable Long productId,
@RequestParam(defaultValue = "1") Integer quantity,
HttpSession session) {
String sessionId = getSessionId(session);
cartService.addProductToCart(sessionId, productId, quantity);
return "redirect:/cart";
}
@PostMapping("/add/service/{serviceId}")
public String addServiceToCart(
@PathVariable Long serviceId,
@RequestParam Long scheduleSlotId,
HttpSession session) {
String sessionId = getSessionId(session);
cartService.addServiceToCart(sessionId, serviceId, scheduleSlotId);
return "redirect:/cart";
}
@PostMapping("/update/{cartItemId}")
public String updateCartItem(
@PathVariable Long cartItemId,
@RequestParam Integer quantity,
HttpSession session) {
cartService.updateCartItemQuantity(cartItemId, quantity);
return "redirect:/cart";
}
@PostMapping("/remove/{cartItemId}")
public String removeCartItem(@PathVariable Long cartItemId) {
cartService.removeCartItem(cartItemId);
return "redirect:/cart";
}
}

View File

@ -1,93 +0,0 @@
package org.prole.shop.controller;
import org.prole.shop.model.CartItem;
import org.prole.shop.model.Order;
import org.prole.shop.service.CartService;
import org.prole.shop.service.OrderService;
import org.prole.shop.service.ShippingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import jakarta.servlet.http.HttpSession;
import java.math.BigDecimal;
import java.util.List;
@Controller
@RequestMapping("/checkout")
public class CheckoutController {
@Autowired
private CartService cartService;
@Autowired
private OrderService orderService;
@Autowired
private ShippingService shippingService;
private String getSessionId(HttpSession session) {
String sessionId = (String) session.getAttribute("sessionId");
if (sessionId == null) {
sessionId = java.util.UUID.randomUUID().toString();
session.setAttribute("sessionId", sessionId);
}
return sessionId;
}
@GetMapping
public String checkout(HttpSession session, Model model) {
String sessionId = getSessionId(session);
List<CartItem> cartItems = cartService.getCartItems(sessionId);
if (cartItems.isEmpty()) {
return "redirect:/cart";
}
BigDecimal subtotal = cartService.getCartTotal(sessionId);
BigDecimal shippingCost = shippingService.calculateShippingCost(cartItems, subtotal);
BigDecimal tax = subtotal.add(shippingCost).multiply(new BigDecimal("0.08"));
BigDecimal total = subtotal.add(shippingCost).add(tax);
model.addAttribute("cartItems", cartItems);
model.addAttribute("subtotal", subtotal);
model.addAttribute("shippingCost", shippingCost);
model.addAttribute("tax", tax);
model.addAttribute("total", total);
model.addAttribute("order", new Order());
return "checkout/form";
}
@PostMapping
public String processCheckout(
@ModelAttribute Order order,
HttpSession session,
RedirectAttributes redirectAttributes) {
String sessionId = getSessionId(session);
try {
Order createdOrder = orderService.createOrderFromCart(sessionId, order);
// Process payment (placeholder - TBD)
String paymentTransactionId = "TXN-" + System.currentTimeMillis();
orderService.processPayment(createdOrder, paymentTransactionId, "Credit Card");
redirectAttributes.addFlashAttribute("orderNumber", createdOrder.getOrderNumber());
return "redirect:/checkout/success";
} catch (Exception e) {
redirectAttributes.addFlashAttribute("error", "Error processing order: " + e.getMessage());
return "redirect:/checkout";
}
}
@GetMapping("/success")
public String checkoutSuccess(@RequestParam(required = false) String orderNumber, Model model) {
if (orderNumber != null) {
model.addAttribute("orderNumber", orderNumber);
}
return "checkout/success";
}
}

View File

@ -1,41 +0,0 @@
package org.prole.shop.controller;
import org.prole.shop.model.Product;
import org.prole.shop.model.Service;
import org.prole.shop.service.ProductService;
import org.prole.shop.service.ServiceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
@Controller
public class HomeController {
@Autowired
private ProductService productService;
@Autowired
private ServiceService serviceService;
@GetMapping("/")
public String home(Model model) {
List<Product> featuredProducts = productService.getAllActiveProducts();
List<Service> featuredServices = serviceService.getAllActiveServices();
// Limit to 6 items for homepage
if (featuredProducts.size() > 6) {
featuredProducts = featuredProducts.subList(0, 6);
}
if (featuredServices.size() > 6) {
featuredServices = featuredServices.subList(0, 6);
}
model.addAttribute("featuredProducts", featuredProducts);
model.addAttribute("featuredServices", featuredServices);
return "index";
}
}

View File

@ -1,35 +0,0 @@
package org.prole.shop.controller;
import org.prole.shop.model.Product;
import org.prole.shop.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@Controller
@RequestMapping("/products")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping
public String listProducts(Model model) {
List<Product> products = productService.getAllActiveProducts();
model.addAttribute("products", products);
return "products/list";
}
@GetMapping("/{id}")
public String productDetail(@PathVariable Long id, Model model) {
Product product = productService.getProductById(id)
.orElseThrow(() -> new RuntimeException("Product not found"));
model.addAttribute("product", product);
return "products/detail";
}
}

View File

@ -1,51 +0,0 @@
package org.prole.shop.controller;
import org.prole.shop.model.Service;
import org.prole.shop.model.ScheduleSlot;
import org.prole.shop.service.CalendarService;
import org.prole.shop.service.ServiceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.time.LocalDateTime;
import java.util.List;
@Controller
@RequestMapping("/services")
public class ServiceController {
@Autowired
private ServiceService serviceService;
@Autowired
private CalendarService calendarService;
@GetMapping
public String listServices(Model model) {
List<Service> services = serviceService.getAllActiveServices();
model.addAttribute("services", services);
return "services/list";
}
@GetMapping("/{id}")
public String serviceDetail(
@PathVariable Long id,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate,
Model model) {
Service service = serviceService.getServiceById(id)
.orElseThrow(() -> new RuntimeException("Service not found"));
List<ScheduleSlot> availableSlots = calendarService.getAvailableSlots(service, startDate, endDate);
model.addAttribute("service", service);
model.addAttribute("availableSlots", availableSlots);
return "services/detail";
}
}

View File

@ -1,146 +0,0 @@
package org.prole.shop.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "cart_items")
public class CartItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String sessionId; // For session-based cart
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "service_id")
private Service service;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "schedule_slot_id")
private ScheduleSlot scheduleSlot;
@Column(nullable = false)
private Integer quantity = 1;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal unitPrice;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal totalPrice;
@Column(nullable = false)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
calculateTotalPrice();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
calculateTotalPrice();
}
private void calculateTotalPrice() {
if (unitPrice != null && quantity != null) {
totalPrice = unitPrice.multiply(BigDecimal.valueOf(quantity));
}
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getSessionId() {
return sessionId;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
public Service getService() {
return service;
}
public void setService(Service service) {
this.service = service;
}
public ScheduleSlot getScheduleSlot() {
return scheduleSlot;
}
public void setScheduleSlot(ScheduleSlot scheduleSlot) {
this.scheduleSlot = scheduleSlot;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
calculateTotalPrice();
}
public BigDecimal getUnitPrice() {
return unitPrice;
}
public void setUnitPrice(BigDecimal unitPrice) {
this.unitPrice = unitPrice;
calculateTotalPrice();
}
public BigDecimal getTotalPrice() {
return totalPrice;
}
public void setTotalPrice(BigDecimal totalPrice) {
this.totalPrice = totalPrice;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@ -1,290 +0,0 @@
package org.prole.shop.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String orderNumber;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal subtotal;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal shippingCost;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal tax;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal total;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private OrderStatus status = OrderStatus.PENDING;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private PaymentStatus paymentStatus = PaymentStatus.PENDING;
// Customer information
@Column(nullable = false)
private String customerName;
@Column(nullable = false)
private String customerEmail;
private String customerPhone;
// Shipping address
private String shippingAddressLine1;
private String shippingAddressLine2;
private String shippingCity;
private String shippingState;
private String shippingPostalCode;
private String shippingCountry;
// Payment information
private String paymentProcessorTransactionId;
private String paymentMethod;
@Column(nullable = false)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime shippedAt;
private LocalDateTime deliveredAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
if (orderNumber == null) {
orderNumber = generateOrderNumber();
}
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
private String generateOrderNumber() {
return "ORD-" + System.currentTimeMillis();
}
public enum OrderStatus {
PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED
}
public enum PaymentStatus {
PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getOrderNumber() {
return orderNumber;
}
public void setOrderNumber(String orderNumber) {
this.orderNumber = orderNumber;
}
public List<OrderItem> getItems() {
return items;
}
public void setItems(List<OrderItem> items) {
this.items = items;
}
public BigDecimal getSubtotal() {
return subtotal;
}
public void setSubtotal(BigDecimal subtotal) {
this.subtotal = subtotal;
}
public BigDecimal getShippingCost() {
return shippingCost;
}
public void setShippingCost(BigDecimal shippingCost) {
this.shippingCost = shippingCost;
}
public BigDecimal getTax() {
return tax;
}
public void setTax(BigDecimal tax) {
this.tax = tax;
}
public BigDecimal getTotal() {
return total;
}
public void setTotal(BigDecimal total) {
this.total = total;
}
public OrderStatus getStatus() {
return status;
}
public void setStatus(OrderStatus status) {
this.status = status;
}
public PaymentStatus getPaymentStatus() {
return paymentStatus;
}
public void setPaymentStatus(PaymentStatus paymentStatus) {
this.paymentStatus = paymentStatus;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public String getCustomerEmail() {
return customerEmail;
}
public void setCustomerEmail(String customerEmail) {
this.customerEmail = customerEmail;
}
public String getCustomerPhone() {
return customerPhone;
}
public void setCustomerPhone(String customerPhone) {
this.customerPhone = customerPhone;
}
public String getShippingAddressLine1() {
return shippingAddressLine1;
}
public void setShippingAddressLine1(String shippingAddressLine1) {
this.shippingAddressLine1 = shippingAddressLine1;
}
public String getShippingAddressLine2() {
return shippingAddressLine2;
}
public void setShippingAddressLine2(String shippingAddressLine2) {
this.shippingAddressLine2 = shippingAddressLine2;
}
public String getShippingCity() {
return shippingCity;
}
public void setShippingCity(String shippingCity) {
this.shippingCity = shippingCity;
}
public String getShippingState() {
return shippingState;
}
public void setShippingState(String shippingState) {
this.shippingState = shippingState;
}
public String getShippingPostalCode() {
return shippingPostalCode;
}
public void setShippingPostalCode(String shippingPostalCode) {
this.shippingPostalCode = shippingPostalCode;
}
public String getShippingCountry() {
return shippingCountry;
}
public void setShippingCountry(String shippingCountry) {
this.shippingCountry = shippingCountry;
}
public String getPaymentProcessorTransactionId() {
return paymentProcessorTransactionId;
}
public void setPaymentProcessorTransactionId(String paymentProcessorTransactionId) {
this.paymentProcessorTransactionId = paymentProcessorTransactionId;
}
public String getPaymentMethod() {
return paymentMethod;
}
public void setPaymentMethod(String paymentMethod) {
this.paymentMethod = paymentMethod;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getShippedAt() {
return shippedAt;
}
public void setShippedAt(LocalDateTime shippedAt) {
this.shippedAt = shippedAt;
}
public LocalDateTime getDeliveredAt() {
return deliveredAt;
}
public void setDeliveredAt(LocalDateTime deliveredAt) {
this.deliveredAt = deliveredAt;
}
}

View File

@ -1,114 +0,0 @@
package org.prole.shop.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "order_items")
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", nullable = false)
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "service_id")
private Service service;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "schedule_slot_id")
private ScheduleSlot scheduleSlot;
@Column(nullable = false)
private Integer quantity;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal unitPrice;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal totalPrice;
private String itemName; // Snapshot of item name at time of order
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Order getOrder() {
return order;
}
public void setOrder(Order order) {
this.order = order;
}
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
public Service getService() {
return service;
}
public void setService(Service service) {
this.service = service;
}
public ScheduleSlot getScheduleSlot() {
return scheduleSlot;
}
public void setScheduleSlot(ScheduleSlot scheduleSlot) {
this.scheduleSlot = scheduleSlot;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public BigDecimal getUnitPrice() {
return unitPrice;
}
public void setUnitPrice(BigDecimal unitPrice) {
this.unitPrice = unitPrice;
}
public BigDecimal getTotalPrice() {
return totalPrice;
}
public void setTotalPrice(BigDecimal totalPrice) {
this.totalPrice = totalPrice;
}
public String getItemName() {
return itemName;
}
public void setItemName(String itemName) {
this.itemName = itemName;
}
}

View File

@ -1,169 +0,0 @@
package org.prole.shop.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(length = 2000)
private String description;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@Column(nullable = false)
private Integer stockQuantity;
private String sku;
private String imageUrl;
@Column(nullable = false)
private Boolean active = true;
@Column(nullable = false)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// Shipping properties
private BigDecimal weight; // in kg
private BigDecimal length; // in cm
private BigDecimal width; // in cm
private BigDecimal height; // in cm
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
public Integer getStockQuantity() {
return stockQuantity;
}
public void setStockQuantity(Integer stockQuantity) {
this.stockQuantity = stockQuantity;
}
public String getSku() {
return sku;
}
public void setSku(String sku) {
this.sku = sku;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public Boolean getActive() {
return active;
}
public void setActive(Boolean active) {
this.active = active;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public BigDecimal getWeight() {
return weight;
}
public void setWeight(BigDecimal weight) {
this.weight = weight;
}
public BigDecimal getLength() {
return length;
}
public void setLength(BigDecimal length) {
this.length = length;
}
public BigDecimal getWidth() {
return width;
}
public void setWidth(BigDecimal width) {
this.width = width;
}
public BigDecimal getHeight() {
return height;
}
public void setHeight(BigDecimal height) {
this.height = height;
}
}

View File

@ -1,100 +0,0 @@
package org.prole.shop.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "schedule_slots")
public class ScheduleSlot {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "service_id", nullable = false)
private Service service;
@Column(nullable = false)
private LocalDateTime startTime;
@Column(nullable = false)
private LocalDateTime endTime;
@Column(nullable = false)
private Boolean available = true;
@Column(nullable = false)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Service getService() {
return service;
}
public void setService(Service service) {
this.service = service;
}
public LocalDateTime getStartTime() {
return startTime;
}
public void setStartTime(LocalDateTime startTime) {
this.startTime = startTime;
}
public LocalDateTime getEndTime() {
return endTime;
}
public void setEndTime(LocalDateTime endTime) {
this.endTime = endTime;
}
public Boolean getAvailable() {
return available;
}
public void setAvailable(Boolean available) {
this.available = available;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@ -1,151 +0,0 @@
package org.prole.shop.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDateTime;
@Entity
@Table(name = "services")
public class Service {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(length = 2000)
private String description;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal pricePerHour;
@Column(nullable = false)
private Duration defaultDuration; // Default service duration
private String imageUrl;
@Column(nullable = false)
private Boolean active = true;
@Column(nullable = false)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// Service provider information
private String providerName;
private String providerEmail;
private String providerPhone;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public BigDecimal getPricePerHour() {
return pricePerHour;
}
public void setPricePerHour(BigDecimal pricePerHour) {
this.pricePerHour = pricePerHour;
}
public Duration getDefaultDuration() {
return defaultDuration;
}
public void setDefaultDuration(Duration defaultDuration) {
this.defaultDuration = defaultDuration;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public Boolean getActive() {
return active;
}
public void setActive(Boolean active) {
this.active = active;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public String getProviderName() {
return providerName;
}
public void setProviderName(String providerName) {
this.providerName = providerName;
}
public String getProviderEmail() {
return providerEmail;
}
public void setProviderEmail(String providerEmail) {
this.providerEmail = providerEmail;
}
public String getProviderPhone() {
return providerPhone;
}
public void setProviderPhone(String providerPhone) {
this.providerPhone = providerPhone;
}
}

View File

@ -1,13 +0,0 @@
package org.prole.shop.repository;
import org.prole.shop.model.CartItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface CartItemRepository extends JpaRepository<CartItem, Long> {
List<CartItem> findBySessionId(String sessionId);
void deleteBySessionId(String sessionId);
}

View File

@ -1,12 +0,0 @@
package org.prole.shop.repository;
import org.prole.shop.model.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
Optional<Order> findByOrderNumber(String orderNumber);
}

View File

@ -1,13 +0,0 @@
package org.prole.shop.repository;
import org.prole.shop.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByActiveTrue();
List<Product> findByActiveTrueOrderByNameAsc();
}

View File

@ -1,25 +0,0 @@
package org.prole.shop.repository;
import org.prole.shop.model.ScheduleSlot;
import org.prole.shop.model.Service;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
@Repository
public interface ScheduleSlotRepository extends JpaRepository<ScheduleSlot, Long> {
List<ScheduleSlot> findByServiceAndAvailableTrue(Service service);
@Query("SELECT s FROM ScheduleSlot s WHERE s.service = :service AND s.available = true AND s.startTime >= :startTime AND s.endTime <= :endTime ORDER BY s.startTime")
List<ScheduleSlot> findAvailableSlotsByServiceAndDateRange(
@Param("service") Service service,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime
);
List<ScheduleSlot> findByServiceAndStartTimeAfterAndAvailableTrue(Service service, LocalDateTime startTime);
}

View File

@ -1,13 +0,0 @@
package org.prole.shop.repository;
import org.prole.shop.model.Service;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ServiceRepository extends JpaRepository<Service, Long> {
List<Service> findByActiveTrue();
List<Service> findByActiveTrueOrderByNameAsc();
}

View File

@ -1,42 +0,0 @@
package org.prole.shop.service;
import org.prole.shop.model.ScheduleSlot;
import org.prole.shop.model.Service;
import org.prole.shop.repository.ScheduleSlotRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Service
@Transactional
public class CalendarService {
@Autowired
private ScheduleSlotRepository scheduleSlotRepository;
public List<ScheduleSlot> getAvailableSlots(Service service, LocalDateTime startDate, LocalDateTime endDate) {
if (startDate != null && endDate != null) {
return scheduleSlotRepository.findAvailableSlotsByServiceAndDateRange(service, startDate, endDate);
} else if (startDate != null) {
return scheduleSlotRepository.findByServiceAndStartTimeAfterAndAvailableTrue(service, startDate);
} else {
return scheduleSlotRepository.findByServiceAndAvailableTrue(service);
}
}
public ScheduleSlot bookSlot(ScheduleSlot slot) {
slot.setAvailable(false);
return scheduleSlotRepository.save(slot);
}
public ScheduleSlot createSlot(ScheduleSlot slot) {
return scheduleSlotRepository.save(slot);
}
public ScheduleSlot getSlotById(Long id) {
return scheduleSlotRepository.findById(id).orElse(null);
}
}

View File

@ -1,114 +0,0 @@
package org.prole.shop.service;
import org.prole.shop.model.CartItem;
import org.prole.shop.model.Product;
import org.prole.shop.model.ScheduleSlot;
import org.prole.shop.model.Service;
import org.prole.shop.repository.CartItemRepository;
import org.prole.shop.repository.ProductRepository;
import org.prole.shop.repository.ScheduleSlotRepository;
import org.prole.shop.repository.ServiceRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
public class CartService {
@Autowired
private CartItemRepository cartItemRepository;
@Autowired
private ProductRepository productRepository;
@Autowired
private ServiceRepository serviceRepository;
@Autowired
private ScheduleSlotRepository scheduleSlotRepository;
public List<CartItem> getCartItems(String sessionId) {
return cartItemRepository.findBySessionId(sessionId);
}
public CartItem addProductToCart(String sessionId, Long productId, Integer quantity) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));
// Check if item already in cart
List<CartItem> existingItems = cartItemRepository.findBySessionId(sessionId);
Optional<CartItem> existingItem = existingItems.stream()
.filter(item -> item.getProduct() != null && item.getProduct().getId().equals(productId))
.findFirst();
if (existingItem.isPresent()) {
CartItem item = existingItem.get();
item.setQuantity(item.getQuantity() + quantity);
return cartItemRepository.save(item);
} else {
CartItem cartItem = new CartItem();
cartItem.setSessionId(sessionId);
cartItem.setProduct(product);
cartItem.setQuantity(quantity);
cartItem.setUnitPrice(product.getPrice());
return cartItemRepository.save(cartItem);
}
}
public CartItem addServiceToCart(String sessionId, Long serviceId, Long scheduleSlotId) {
Service service = serviceRepository.findById(serviceId)
.orElseThrow(() -> new RuntimeException("Service not found"));
ScheduleSlot slot = scheduleSlotRepository.findById(scheduleSlotId)
.orElseThrow(() -> new RuntimeException("Schedule slot not found"));
if (!slot.getAvailable()) {
throw new RuntimeException("Schedule slot is not available");
}
// Calculate price based on duration
Duration duration = Duration.between(slot.getStartTime(), slot.getEndTime());
long hours = duration.toHours();
if (duration.toMinutes() % 60 > 0) {
hours += 1; // Round up to next hour
}
BigDecimal totalPrice = service.getPricePerHour().multiply(BigDecimal.valueOf(hours));
CartItem cartItem = new CartItem();
cartItem.setSessionId(sessionId);
cartItem.setService(service);
cartItem.setScheduleSlot(slot);
cartItem.setQuantity(1);
cartItem.setUnitPrice(totalPrice);
return cartItemRepository.save(cartItem);
}
public void removeCartItem(Long cartItemId) {
cartItemRepository.deleteById(cartItemId);
}
public void updateCartItemQuantity(Long cartItemId, Integer quantity) {
CartItem item = cartItemRepository.findById(cartItemId)
.orElseThrow(() -> new RuntimeException("Cart item not found"));
item.setQuantity(quantity);
cartItemRepository.save(item);
}
public void clearCart(String sessionId) {
cartItemRepository.deleteBySessionId(sessionId);
}
public BigDecimal getCartTotal(String sessionId) {
List<CartItem> items = getCartItems(sessionId);
return items.stream()
.map(CartItem::getTotalPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}

View File

@ -1,128 +0,0 @@
package org.prole.shop.service;
import org.prole.shop.model.*;
import org.prole.shop.repository.CartItemRepository;
import org.prole.shop.repository.OrderRepository;
import org.prole.shop.repository.ScheduleSlotRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Transactional
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private CartItemRepository cartItemRepository;
@Autowired
private ShippingService shippingService;
@Autowired
private ScheduleSlotRepository scheduleSlotRepository;
private static final BigDecimal TAX_RATE = new BigDecimal("0.08"); // 8% tax rate
public Order createOrderFromCart(String sessionId, Order orderRequest) {
List<CartItem> cartItems = cartItemRepository.findBySessionId(sessionId);
if (cartItems.isEmpty()) {
throw new RuntimeException("Cart is empty");
}
Order order = new Order();
order.setCustomerName(orderRequest.getCustomerName());
order.setCustomerEmail(orderRequest.getCustomerEmail());
order.setCustomerPhone(orderRequest.getCustomerPhone());
order.setShippingAddressLine1(orderRequest.getShippingAddressLine1());
order.setShippingAddressLine2(orderRequest.getShippingAddressLine2());
order.setShippingCity(orderRequest.getShippingCity());
order.setShippingState(orderRequest.getShippingState());
order.setShippingPostalCode(orderRequest.getShippingPostalCode());
order.setShippingCountry(orderRequest.getShippingCountry());
// Calculate subtotal
BigDecimal subtotal = cartItems.stream()
.map(CartItem::getTotalPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
order.setSubtotal(subtotal);
// Calculate shipping
BigDecimal shippingCost = shippingService.calculateShippingCost(cartItems, subtotal);
order.setShippingCost(shippingCost);
// Calculate tax
BigDecimal taxableAmount = subtotal.add(shippingCost);
BigDecimal tax = taxableAmount.multiply(TAX_RATE);
order.setTax(tax);
// Calculate total
BigDecimal total = subtotal.add(shippingCost).add(tax);
order.setTotal(total);
// Convert cart items to order items
List<OrderItem> orderItems = cartItems.stream().map(cartItem -> {
OrderItem orderItem = new OrderItem();
orderItem.setOrder(order);
orderItem.setProduct(cartItem.getProduct());
orderItem.setService(cartItem.getService());
orderItem.setScheduleSlot(cartItem.getScheduleSlot());
orderItem.setQuantity(cartItem.getQuantity());
orderItem.setUnitPrice(cartItem.getUnitPrice());
orderItem.setTotalPrice(cartItem.getTotalPrice());
// Store item name snapshot
if (cartItem.getProduct() != null) {
orderItem.setItemName(cartItem.getProduct().getName());
} else if (cartItem.getService() != null) {
orderItem.setItemName(cartItem.getService().getName());
}
return orderItem;
}).collect(Collectors.toList());
order.setItems(orderItems);
// Mark schedule slots as unavailable
cartItems.stream()
.filter(item -> item.getScheduleSlot() != null)
.forEach(item -> {
ScheduleSlot slot = item.getScheduleSlot();
slot.setAvailable(false);
scheduleSlotRepository.save(slot);
});
Order savedOrder = orderRepository.save(order);
// Clear cart
cartItemRepository.deleteBySessionId(sessionId);
return savedOrder;
}
public Order processPayment(Order order, String paymentProcessorTransactionId, String paymentMethod) {
order.setPaymentProcessorTransactionId(paymentProcessorTransactionId);
order.setPaymentMethod(paymentMethod);
order.setPaymentStatus(Order.PaymentStatus.PROCESSING);
order.setStatus(Order.OrderStatus.PROCESSING);
// TODO: Integrate with actual payment processor
// For now, simulate successful payment
order.setPaymentStatus(Order.PaymentStatus.COMPLETED);
order.setStatus(Order.OrderStatus.PROCESSING);
return orderRepository.save(order);
}
public Order getOrderByNumber(String orderNumber) {
return orderRepository.findByOrderNumber(orderNumber)
.orElseThrow(() -> new RuntimeException("Order not found"));
}
}

View File

@ -1,34 +0,0 @@
package org.prole.shop.service;
import org.prole.shop.model.Product;
import org.prole.shop.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
public class ProductService {
@Autowired
private ProductRepository productRepository;
public List<Product> getAllActiveProducts() {
return productRepository.findByActiveTrueOrderByNameAsc();
}
public Optional<Product> getProductById(Long id) {
return productRepository.findById(id);
}
public Product saveProduct(Product product) {
return productRepository.save(product);
}
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
}

View File

@ -1,34 +0,0 @@
package org.prole.shop.service;
import org.prole.shop.model.Service;
import org.prole.shop.repository.ServiceRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
public class ServiceService {
@Autowired
private ServiceRepository serviceRepository;
public List<Service> getAllActiveServices() {
return serviceRepository.findByActiveTrueOrderByNameAsc();
}
public Optional<Service> getServiceById(Long id) {
return serviceRepository.findById(id);
}
public Service saveService(Service service) {
return serviceRepository.save(service);
}
public void deleteService(Long id) {
serviceRepository.deleteById(id);
}
}

View File

@ -1,44 +0,0 @@
package org.prole.shop.service;
import org.prole.shop.model.CartItem;
import org.prole.shop.model.Product;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;
@Service
public class ShippingService {
// Base shipping rates (can be configured)
private static final BigDecimal BASE_SHIPPING_COST = new BigDecimal("10.00");
private static final BigDecimal COST_PER_KG = new BigDecimal("2.00");
private static final BigDecimal FREE_SHIPPING_THRESHOLD = new BigDecimal("100.00");
public BigDecimal calculateShippingCost(List<CartItem> cartItems, BigDecimal subtotal) {
// Free shipping for orders over threshold
if (subtotal.compareTo(FREE_SHIPPING_THRESHOLD) >= 0) {
return BigDecimal.ZERO;
}
// Calculate total weight
BigDecimal totalWeight = BigDecimal.ZERO;
for (CartItem item : cartItems) {
if (item.getProduct() != null && item.getProduct().getWeight() != null) {
BigDecimal itemWeight = item.getProduct().getWeight()
.multiply(BigDecimal.valueOf(item.getQuantity()));
totalWeight = totalWeight.add(itemWeight);
}
}
// Calculate shipping cost: base + (weight * cost per kg)
BigDecimal weightCost = totalWeight.multiply(COST_PER_KG);
return BASE_SHIPPING_COST.add(weightCost);
}
public String estimateDeliveryDays(List<CartItem> cartItems) {
// Simple estimation: 3-5 business days for standard shipping
// Can be enhanced with actual shipping provider integration
return "3-5 business days";
}
}

View File

@ -1,38 +0,0 @@
# Database Configuration
spring.datasource.url=jdbc:postgresql://prole-db-rw:5432/prole-db
spring.datasource.username=prole
spring.datasource.password=Fr0b0zzg0bl!n!
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.format_sql=true
spring.sql.init.mode=always
spring.sql.init.schema-locations=classpath:schema.sql
spring.sql.init.data-locations=classpath:data.sql
# Server Configuration
server.port=8080
# Thymeleaf Configuration
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.cache=false
spring.thymeleaf.mode=HTML
# Static Resources
spring.web.resources.static-locations=classpath:/static/
spring.mvc.static-path-pattern=/static/**
# Batch/Archive Export
spring.batch.jdbc.initialize-schema=always
# Prevent auto-running jobs at startup; scheduler will launch
spring.batch.job.enabled=false
# Base folder for Avro archives (will create YYYY/MM/DD structure inside)
prole.archive.base-path=storage/archive
# Turn off to disable scheduled exports without code changes
prole.archive.enabled=true

View File

@ -1,40 +0,0 @@
-- Sample data for development/testing
-- This file is executed after schema.sql when spring.jpa.hibernate.ddl-auto is set to create or create-drop
-- Sample Products
INSERT INTO products (name, description, price, stock_quantity, sku, image_url, weight, length, width, height, active) VALUES
('Wireless Headphones', 'Premium wireless headphones with noise cancellation', 99.99, 50, 'WH-001', '/static/images/placeholder.jpg', 0.3, 20, 18, 8, true),
('Smart Watch', 'Feature-rich smartwatch with fitness tracking', 249.99, 30, 'SW-002', '/static/images/placeholder.jpg', 0.05, 4, 4, 1, true),
('Laptop Stand', 'Ergonomic aluminum laptop stand', 49.99, 100, 'LS-003', '/static/images/placeholder.jpg', 0.8, 30, 25, 5, true),
('USB-C Cable', 'High-speed USB-C charging cable, 6ft', 19.99, 200, 'UC-004', '/static/images/placeholder.jpg', 0.1, 180, 1, 1, true),
('Wireless Mouse', 'Ergonomic wireless mouse with long battery life', 29.99, 75, 'WM-005', '/static/images/placeholder.jpg', 0.1, 10, 6, 4, true),
('Desk Organizer', 'Bamboo desk organizer with multiple compartments', 34.99, 60, 'DO-006', '/static/images/placeholder.jpg', 0.5, 30, 20, 10, true);
-- Sample Services
INSERT INTO services (name, description, price_per_hour, default_duration, image_url, provider_name, provider_email, provider_phone, active) VALUES
('Web Development Consultation', 'Expert consultation on web development projects and architecture', 150.00, INTERVAL '1 hour', '/static/images/placeholder.jpg', 'John Developer', 'john@example.com', '555-0101', true),
('Graphic Design Service', 'Professional graphic design for logos, branding, and marketing materials', 120.00, INTERVAL '2 hours', '/static/images/placeholder.jpg', 'Sarah Designer', 'sarah@example.com', '555-0102', true),
('IT Support', 'On-site or remote IT support and troubleshooting', 100.00, INTERVAL '1 hour', '/static/images/placeholder.jpg', 'Mike Technician', 'mike@example.com', '555-0103', true),
('Business Consulting', 'Strategic business consulting and planning', 200.00, INTERVAL '2 hours', '/static/images/placeholder.jpg', 'Lisa Consultant', 'lisa@example.com', '555-0104', true),
('Photography Session', 'Professional photography for events, portraits, or products', 175.00, INTERVAL '3 hours', '/static/images/placeholder.jpg', 'Alex Photographer', 'alex@example.com', '555-0105', true),
('Content Writing', 'Professional content writing for websites, blogs, and marketing', 80.00, INTERVAL '1 hour', '/static/images/placeholder.jpg', 'Emma Writer', 'emma@example.com', '555-0106', true);
-- Sample Schedule Slots (for the next 30 days)
-- Web Development Consultation slots
INSERT INTO schedule_slots (service_id, start_time, end_time, available)
SELECT 1,
CURRENT_DATE + INTERVAL '1 day' + (n || ' hours')::INTERVAL,
CURRENT_DATE + INTERVAL '1 day' + ((n + 1) || ' hours')::INTERVAL,
true
FROM generate_series(9, 16) n
WHERE CURRENT_DATE + INTERVAL '1 day' + (n || ' hours')::INTERVAL > CURRENT_TIMESTAMP;
-- Graphic Design Service slots
INSERT INTO schedule_slots (service_id, start_time, end_time, available)
SELECT 2,
CURRENT_DATE + INTERVAL '2 days' + (n || ' hours')::INTERVAL,
CURRENT_DATE + INTERVAL '2 days' + ((n + 2) || ' hours')::INTERVAL,
true
FROM generate_series(10, 15) n
WHERE CURRENT_DATE + INTERVAL '2 days' + (n || ' hours')::INTERVAL > CURRENT_TIMESTAMP;

View File

@ -1,119 +0,0 @@
-- Prole Shop Database Schema
-- This file contains the initial database schema for the shopping application
-- Products table
CREATE TABLE IF NOT EXISTS products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price NUMERIC(10, 2) NOT NULL,
stock_quantity INTEGER NOT NULL,
sku VARCHAR(100),
image_url VARCHAR(500),
active BOOLEAN NOT NULL DEFAULT true,
weight NUMERIC(10, 2),
length NUMERIC(10, 2),
width NUMERIC(10, 2),
height NUMERIC(10, 2),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP
);
-- Services table
CREATE TABLE IF NOT EXISTS services (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price_per_hour NUMERIC(10, 2) NOT NULL,
default_duration INTERVAL NOT NULL,
image_url VARCHAR(500),
active BOOLEAN NOT NULL DEFAULT true,
provider_name VARCHAR(255),
provider_email VARCHAR(255),
provider_phone VARCHAR(50),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP
);
-- Schedule slots table
CREATE TABLE IF NOT EXISTS schedule_slots (
id BIGSERIAL PRIMARY KEY,
service_id BIGINT NOT NULL REFERENCES services(id) ON DELETE CASCADE,
start_time TIMESTAMP NOT NULL,
end_time TIMESTAMP NOT NULL,
available BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP
);
-- Cart items table
CREATE TABLE IF NOT EXISTS cart_items (
id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(255) NOT NULL,
product_id BIGINT REFERENCES products(id) ON DELETE CASCADE,
service_id BIGINT REFERENCES services(id) ON DELETE CASCADE,
schedule_slot_id BIGINT REFERENCES schedule_slots(id) ON DELETE CASCADE,
quantity INTEGER NOT NULL DEFAULT 1,
unit_price NUMERIC(10, 2) NOT NULL,
total_price NUMERIC(10, 2) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP,
CONSTRAINT cart_item_check CHECK (
(product_id IS NOT NULL AND service_id IS NULL) OR
(product_id IS NULL AND service_id IS NOT NULL)
)
);
-- Orders table
CREATE TABLE IF NOT EXISTS orders (
id BIGSERIAL PRIMARY KEY,
order_number VARCHAR(255) NOT NULL UNIQUE,
subtotal NUMERIC(10, 2) NOT NULL,
shipping_cost NUMERIC(10, 2) NOT NULL,
tax NUMERIC(10, 2) NOT NULL,
total NUMERIC(10, 2) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
payment_status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
customer_name VARCHAR(255) NOT NULL,
customer_email VARCHAR(255) NOT NULL,
customer_phone VARCHAR(50),
shipping_address_line1 VARCHAR(255),
shipping_address_line2 VARCHAR(255),
shipping_city VARCHAR(100),
shipping_state VARCHAR(100),
shipping_postal_code VARCHAR(20),
shipping_country VARCHAR(100),
payment_processor_transaction_id VARCHAR(255),
payment_method VARCHAR(50),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP,
shipped_at TIMESTAMP,
delivered_at TIMESTAMP
);
-- Order items table
CREATE TABLE IF NOT EXISTS order_items (
id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id BIGINT REFERENCES products(id) ON DELETE SET NULL,
service_id BIGINT REFERENCES services(id) ON DELETE SET NULL,
schedule_slot_id BIGINT REFERENCES schedule_slots(id) ON DELETE SET NULL,
quantity INTEGER NOT NULL,
unit_price NUMERIC(10, 2) NOT NULL,
total_price NUMERIC(10, 2) NOT NULL,
item_name VARCHAR(255),
CONSTRAINT order_item_check CHECK (
(product_id IS NOT NULL AND service_id IS NULL) OR
(product_id IS NULL AND service_id IS NOT NULL)
)
);
-- Create indexes for better performance
CREATE INDEX IF NOT EXISTS idx_products_active ON products(active);
CREATE INDEX IF NOT EXISTS idx_services_active ON services(active);
CREATE INDEX IF NOT EXISTS idx_schedule_slots_service ON schedule_slots(service_id);
CREATE INDEX IF NOT EXISTS idx_schedule_slots_available ON schedule_slots(available, start_time);
CREATE INDEX IF NOT EXISTS idx_cart_items_session ON cart_items(session_id);
CREATE INDEX IF NOT EXISTS idx_orders_order_number ON orders(order_number);
CREATE INDEX IF NOT EXISTS idx_order_items_order ON order_items(order_id);

View File

@ -1 +0,0 @@
PLACEHOLDER

View File

@ -1,91 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::content}, 'Shopping Cart')}">
<body>
<div th:fragment="content">
<h1 class="mb-4">Shopping Cart</h1>
<div th:if="${cartItems != null and !cartItems.isEmpty()}">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Item</th>
<th>Type</th>
<th>Price</th>
<th>Quantity</th>
<th>Total</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr th:each="item : ${cartItems}">
<td>
<strong th:text="${item.product != null ? item.product.name : (item.service != null ? item.service.name : 'N/A')}"></strong>
<div th:if="${item.scheduleSlot != null}" class="small text-muted">
<span th:text="${#temporals.format(item.scheduleSlot.startTime, 'MMM dd, yyyy HH:mm')}"></span>
</div>
</td>
<td>
<span th:if="${item.product != null}" class="badge bg-primary">Product</span>
<span th:if="${item.service != null}" class="badge bg-success">Service</span>
</td>
<td th:text="${'$' + #numbers.formatDecimal(item.unitPrice, 1, 2)}"></td>
<td>
<form th:action="@{/cart/update/{id}(id=${item.id})}" method="post" class="d-inline">
<input type="number" name="quantity" th:value="${item.quantity}" min="1"
class="form-control form-control-sm d-inline-block" style="width: 80px;"
onchange="this.form.submit()">
</form>
</td>
<td th:text="${'$' + #numbers.formatDecimal(item.totalPrice, 1, 2)}"></td>
<td>
<form th:action="@{/cart/remove/{id}(id=${item.id})}" method="post" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
</tbody>
</table>
</div>
<div class="row mt-4">
<div class="col-md-6 offset-md-6">
<div class="card">
<div class="card-body">
<h5 class="card-title">Order Summary</h5>
<table class="table table-sm">
<tr>
<td>Subtotal:</td>
<td class="text-end" th:text="${'$' + #numbers.formatDecimal(subtotal, 1, 2)}"></td>
</tr>
<tr>
<td>Shipping:</td>
<td class="text-end" th:text="${'$' + #numbers.formatDecimal(shippingCost, 1, 2)}"></td>
</tr>
<tr>
<td>Tax:</td>
<td class="text-end" th:text="${'$' + #numbers.formatDecimal(tax, 1, 2)}"></td>
</tr>
<tr class="table-active">
<td><strong>Total:</strong></td>
<td class="text-end"><strong th:text="${'$' + #numbers.formatDecimal(total, 1, 2)}"></strong></td>
</tr>
</table>
<p class="small text-muted" th:text="${'Estimated delivery: ' + estimatedDelivery}"></p>
<a th:href="@{/checkout}" class="btn btn-primary btn-lg w-100">Proceed to Checkout</a>
</div>
</div>
</div>
</div>
</div>
<div th:if="${cartItems == null or cartItems.isEmpty()}" class="alert alert-info">
<h4>Your cart is empty</h4>
<p>Start shopping by browsing our <a th:href="@{/products}">products</a> or <a th:href="@{/services}">services</a>.</p>
</div>
</div>
</body>
</html>

View File

@ -1,114 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::content}, 'Checkout')}">
<body>
<div th:fragment="content">
<h1 class="mb-4">Checkout</h1>
<div class="row">
<div class="col-md-8">
<form th:action="@{/checkout}" th:object="${order}" method="post">
<div class="card mb-4">
<div class="card-header">
<h5>Customer Information</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="customerName" class="form-label">Full Name *</label>
<input type="text" class="form-control" id="customerName" th:field="*{customerName}" required>
</div>
<div class="mb-3">
<label for="customerEmail" class="form-label">Email *</label>
<input type="email" class="form-control" id="customerEmail" th:field="*{customerEmail}" required>
</div>
<div class="mb-3">
<label for="customerPhone" class="form-label">Phone</label>
<input type="tel" class="form-control" id="customerPhone" th:field="*{customerPhone}">
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5>Shipping Address</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="shippingAddressLine1" class="form-label">Address Line 1 *</label>
<input type="text" class="form-control" id="shippingAddressLine1" th:field="*{shippingAddressLine1}" required>
</div>
<div class="mb-3">
<label for="shippingAddressLine2" class="form-label">Address Line 2</label>
<input type="text" class="form-control" id="shippingAddressLine2" th:field="*{shippingAddressLine2}">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="shippingCity" class="form-label">City *</label>
<input type="text" class="form-control" id="shippingCity" th:field="*{shippingCity}" required>
</div>
<div class="col-md-6 mb-3">
<label for="shippingState" class="form-label">State/Province *</label>
<input type="text" class="form-control" id="shippingState" th:field="*{shippingState}" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="shippingPostalCode" class="form-label">Postal Code *</label>
<input type="text" class="form-control" id="shippingPostalCode" th:field="*{shippingPostalCode}" required>
</div>
<div class="col-md-6 mb-3">
<label for="shippingCountry" class="form-label">Country *</label>
<input type="text" class="form-control" id="shippingCountry" th:field="*{shippingCountry}" required>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5>Payment Information</h5>
</div>
<div class="card-body">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> Payment processing will be integrated with an external payment processor (TBD).
For now, this is a demonstration checkout flow.
</div>
<p class="text-muted">Payment method: Credit Card (TBD)</p>
</div>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100">Place Order</button>
</form>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5>Order Summary</h5>
</div>
<div class="card-body">
<table class="table table-sm">
<tr>
<td>Subtotal:</td>
<td class="text-end" th:text="${'$' + #numbers.formatDecimal(subtotal, 1, 2)}"></td>
</tr>
<tr>
<td>Shipping:</td>
<td class="text-end" th:text="${'$' + #numbers.formatDecimal(shippingCost, 1, 2)}"></td>
</tr>
<tr>
<td>Tax:</td>
<td class="text-end" th:text="${'$' + #numbers.formatDecimal(tax, 1, 2)}"></td>
</tr>
<tr class="table-active">
<td><strong>Total:</strong></td>
<td class="text-end"><strong th:text="${'$' + #numbers.formatDecimal(total, 1, 2)}"></strong></td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,21 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::content}, 'Order Confirmation')}">
<body>
<div th:fragment="content">
<div class="text-center">
<div class="mb-4">
<i class="bi bi-check-circle-fill text-success" style="font-size: 4rem;"></i>
</div>
<h1 class="mb-3">Order Confirmed!</h1>
<p class="lead" th:if="${orderNumber}">
Your order number is: <strong th:text="${orderNumber}"></strong>
</p>
<p class="text-muted mb-4">
Thank you for your purchase. You will receive a confirmation email shortly.
</p>
<a th:href="@{/}" class="btn btn-primary btn-lg">Continue Shopping</a>
</div>
</div>
</body>
</html>

View File

@ -1,52 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::content}, 'Home')}">
<body>
<div th:fragment="content">
<div class="jumbotron bg-light p-5 rounded mb-4">
<h1 class="display-4">Welcome to Prole Shop</h1>
<p class="lead">Shop for products and book professional services</p>
</div>
<div th:if="${featuredProducts != null and !featuredProducts.isEmpty()}">
<h2 class="mb-4">Featured Products</h2>
<div class="row">
<div class="col-md-4 mb-4" th:each="product : ${featuredProducts}">
<div class="card product-card">
<img th:src="${product.imageUrl != null ? product.imageUrl : '/static/images/placeholder.jpg'}"
class="card-img-top" alt="Product image" style="height: 200px; object-fit: cover;">
<div class="card-body">
<h5 class="card-title" th:text="${product.name}"></h5>
<p class="card-text" th:text="${product.description != null ? #strings.abbreviate(product.description, 100) : ''}"></p>
<p class="card-text">
<strong class="text-primary" th:text="${'$' + #numbers.formatDecimal(product.price, 1, 2)}"></strong>
</p>
<a th:href="@{/products/{id}(id=${product.id})}" class="btn btn-primary">View Details</a>
</div>
</div>
</div>
</div>
</div>
<div th:if="${featuredServices != null and !featuredServices.isEmpty()}" class="mt-5">
<h2 class="mb-4">Featured Services</h2>
<div class="row">
<div class="col-md-4 mb-4" th:each="service : ${featuredServices}">
<div class="card service-card">
<img th:src="${service.imageUrl != null ? service.imageUrl : '/static/images/placeholder.jpg'}"
class="card-img-top" alt="Service image" style="height: 200px; object-fit: cover;">
<div class="card-body">
<h5 class="card-title" th:text="${service.name}"></h5>
<p class="card-text" th:text="${service.description != null ? #strings.abbreviate(service.description, 100) : ''}"></p>
<p class="card-text">
<strong class="text-primary" th:text="${'$' + #numbers.formatDecimal(service.pricePerHour, 1, 2) + '/hour'}"></strong>
</p>
<a th:href="@{/services/{id}(id=${service.id})}" class="btn btn-primary">View Details</a>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,79 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title th:text="${title != null ? title : 'Prole Shop'}">Prole Shop</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<style>
.navbar-brand {
font-weight: bold;
}
.product-card, .service-card {
transition: transform 0.2s;
height: 100%;
}
.product-card:hover, .service-card:hover {
transform: translateY(-5px);
}
.cart-badge {
position: absolute;
top: -5px;
right: -5px;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" th:href="@{/}">
<i class="bi bi-shop"></i> Prole Shop
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" th:href="@{/}">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{/products}">Products</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{/services}">Services</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link position-relative" th:href="@{/cart}">
<i class="bi bi-cart3"></i> Cart
</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container my-4">
<div th:if="${error}" class="alert alert-danger alert-dismissible fade show" role="alert">
<span th:text="${error}"></span>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<div th:if="${success}" class="alert alert-success alert-dismissible fade show" role="alert">
<span th:text="${success}"></span>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<div th:replace="${content}"></div>
</main>
<footer class="bg-light py-4 mt-5">
<div class="container text-center">
<p class="text-muted mb-0">&copy; 2024 Prole Shop. All rights reserved.</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -1,38 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::content}, ${product.name})}">
<body>
<div th:fragment="content">
<div class="row">
<div class="col-md-6">
<img th:src="${product.imageUrl != null ? product.imageUrl : '/static/images/placeholder.jpg'}"
class="img-fluid rounded" alt="Product image">
</div>
<div class="col-md-6">
<h1 th:text="${product.name}"></h1>
<p class="lead" th:text="${'$' + #numbers.formatDecimal(product.price, 1, 2)}"></p>
<p th:text="${product.description}"></p>
<div class="mb-3">
<strong>SKU:</strong> <span th:text="${product.sku != null ? product.sku : 'N/A'}"></span>
</div>
<div class="mb-3">
<strong>Stock:</strong>
<span th:text="${product.stockQuantity}"></span>
<span th:if="${product.stockQuantity <= 5}" class="badge bg-warning ms-2">Low Stock</span>
</div>
<form th:action="@{/cart/add/product/{id}(id=${product.id})}" method="post" class="mt-4">
<div class="input-group mb-3" style="max-width: 200px;">
<label class="input-group-text" for="quantity">Quantity</label>
<input type="number" class="form-control" id="quantity" name="quantity" value="1" min="1" th:max="${product.stockQuantity}">
</div>
<button type="submit" class="btn btn-primary btn-lg" th:disabled="${product.stockQuantity == 0}">
<i class="bi bi-cart-plus"></i> Add to Cart
</button>
</form>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,26 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::content}, 'Products')}">
<body>
<div th:fragment="content">
<h1 class="mb-4">Products</h1>
<div class="row">
<div class="col-md-4 mb-4" th:each="product : ${products}">
<div class="card product-card">
<img th:src="${product.imageUrl != null ? product.imageUrl : '/static/images/placeholder.jpg'}"
class="card-img-top" alt="Product image" style="height: 200px; object-fit: cover;">
<div class="card-body">
<h5 class="card-title" th:text="${product.name}"></h5>
<p class="card-text" th:text="${product.description != null ? #strings.abbreviate(product.description, 100) : ''}"></p>
<p class="card-text">
<strong class="text-primary" th:text="${'$' + #numbers.formatDecimal(product.price, 1, 2)}"></strong>
<span th:if="${product.stockQuantity <= 5}" class="badge bg-warning ms-2">Low Stock</span>
</p>
<a th:href="@{/products/{id}(id=${product.id})}" class="btn btn-primary">View Details</a>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,61 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::content}, ${service.name})}">
<head>
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.js"></script>
</head>
<body>
<div th:fragment="content">
<div class="row">
<div class="col-md-6">
<img th:src="${service.imageUrl != null ? service.imageUrl : '/static/images/placeholder.jpg'}"
class="img-fluid rounded" alt="Service image">
</div>
<div class="col-md-6">
<h1 th:text="${service.name}"></h1>
<p class="lead" th:text="${'$' + #numbers.formatDecimal(service.pricePerHour, 1, 2) + ' per hour'}"></p>
<p th:text="${service.description}"></p>
<div class="mb-3" th:if="${service.providerName != null}">
<strong>Provider:</strong> <span th:text="${service.providerName}"></span>
</div>
<div class="mb-3" th:if="${service.providerEmail != null}">
<strong>Email:</strong> <span th:text="${service.providerEmail}"></span>
</div>
<div class="mb-3" th:if="${service.providerPhone != null}">
<strong>Phone:</strong> <span th:text="${service.providerPhone}"></span>
</div>
</div>
</div>
<div class="row mt-5">
<div class="col-12">
<h2>Available Time Slots</h2>
<div th:if="${availableSlots != null and !availableSlots.isEmpty()}">
<div class="row">
<div class="col-md-4 mb-3" th:each="slot : ${availableSlots}">
<div class="card">
<div class="card-body">
<h5 class="card-title" th:text="${#temporals.format(slot.startTime, 'MMM dd, yyyy')}"></h5>
<p class="card-text">
<strong>Time:</strong>
<span th:text="${#temporals.format(slot.startTime, 'HH:mm')}"></span> -
<span th:text="${#temporals.format(slot.endTime, 'HH:mm')}"></span>
</p>
<form th:action="@{/cart/add/service/{id}(id=${service.id})}" method="post">
<input type="hidden" name="scheduleSlotId" th:value="${slot.id}">
<button type="submit" class="btn btn-primary">Book This Slot</button>
</form>
</div>
</div>
</div>
</div>
</div>
<div th:if="${availableSlots == null or availableSlots.isEmpty()}" class="alert alert-info">
No available time slots at the moment. Please check back later.
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::content}, 'Services')}">
<body>
<div th:fragment="content">
<h1 class="mb-4">Services</h1>
<div class="row">
<div class="col-md-4 mb-4" th:each="service : ${services}">
<div class="card service-card">
<img th:src="${service.imageUrl != null ? service.imageUrl : '/static/images/placeholder.jpg'}"
class="card-img-top" alt="Service image" style="height: 200px; object-fit: cover;">
<div class="card-body">
<h5 class="card-title" th:text="${service.name}"></h5>
<p class="card-text" th:text="${service.description != null ? #strings.abbreviate(service.description, 100) : ''}"></p>
<p class="card-text">
<strong class="text-primary" th:text="${'$' + #numbers.formatDecimal(service.pricePerHour, 1, 2) + '/hour'}"></strong>
</p>
<a th:href="@{/services/{id}(id=${service.id})}" class="btn btn-primary">View Details & Book</a>
</div>
</div>
</div>
</div>
</div>
</body>
</html>