mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 17:54:32 +00:00
Merge branch 'main' of github.com:dredx/prole
This commit is contained in:
commit
9b50fb963d
1
.gitignore
vendored
1
.gitignore
vendored
@ -44,3 +44,4 @@ htmlcov/
|
||||
/workstation/Prole/Saved/
|
||||
/workstation/Prole/DerivedDataCache/
|
||||
/workstation/Prole/Build/
|
||||
/ssh-keys/
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -1,326 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# init_cloudnative_pg.sh
|
||||
# Purpose:
|
||||
# - Distribute administrator ed25519 key pair to CloudNative‑PG 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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -1,326 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# init_cloudnative_pg.sh
|
||||
# Purpose:
|
||||
# - Distribute administrator ed25519 key pair to CloudNative‑PG 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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -1,326 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# init_cloudnative_pg.sh
|
||||
# Purpose:
|
||||
# - Distribute administrator ed25519 key pair to CloudNative‑PG 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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -1,326 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# init_cloudnative_pg.sh
|
||||
# Purpose:
|
||||
# - Distribute administrator ed25519 key pair to CloudNative‑PG 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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -1,326 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# init_cloudnative_pg.sh
|
||||
# Purpose:
|
||||
# - Distribute administrator ed25519 key pair to CloudNative‑PG 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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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 |
BIN
img/test.png
BIN
img/test.png
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 KiB |
@ -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>
|
||||
|
||||
@ -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>
|
||||
131
www/README.md
131
www/README.md
@ -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 |
@ -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>
|
||||
@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
86
www/pom.xml
86
www/pom.xml
@ -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>
|
||||
|
||||
@ -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>© 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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1 +0,0 @@
|
||||
PLACEHOLDER
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">© 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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user