mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 12:03:59 +00:00
feat: infrastructure and installer updates for k3s, OpenTofu, and prole-db
- Add k3s start/stop Ansible playbooks and roles. - Implement OpenTofu initialization scripts and k8s manifests. - Update ncurses installer with OpenTofu support and improved k3s integration. - Add mode support (--mode) to etc/ initialization scripts. - Update prole-db with recovery, barman objectstore, and SSH OpenBao support. - Refine k8s manifests for OpenBao and prole-db.
This commit is contained in:
parent
82dc879116
commit
da2f6600ba
22
deploy/opentofu/k3s/README.md
Normal file
22
deploy/opentofu/k3s/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# OpenTofu k3s Pipeline
|
||||
|
||||
This pipeline re-deploys the Prole environment into a k3s cluster using OpenTofu.
|
||||
|
||||
## Usage
|
||||
|
||||
1) Ensure `opentofu.auto.tfvars` is populated (install.py will generate it).
|
||||
2) Sync manifests into `deploy/opentofu/k3s/manifests`.
|
||||
3) Run:
|
||||
|
||||
```bash
|
||||
tofu init
|
||||
tofu plan
|
||||
tofu apply
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
- `main.tf`: Applies Kubernetes manifests with the configured namespace.
|
||||
- `variables.tf`: Pipeline inputs (server URL, token, namespace).
|
||||
- `opentofu.auto.tfvars`: Auto-generated values from Prole install/config.
|
||||
- `manifests/`: Copy of `k8s/` manifests to re-deploy.
|
||||
72
deploy/opentofu/k3s/main.tf
Normal file
72
deploy/opentofu/k3s/main.tf
Normal file
@ -0,0 +1,72 @@
|
||||
terraform {
|
||||
required_version = ">= 1.6.0"
|
||||
required_providers {
|
||||
kubernetes = {
|
||||
source = "hashicorp/kubernetes"
|
||||
version = "~> 2.30"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "kubernetes" {
|
||||
host = var.k3s_server_url
|
||||
token = var.k3s_token
|
||||
insecure = true
|
||||
}
|
||||
|
||||
locals {
|
||||
manifest_dir = "${path.module}/manifests"
|
||||
manifest_files = fileset(local.manifest_dir, "**/*.yaml")
|
||||
|
||||
raw_documents = flatten([
|
||||
for f in local.manifest_files : [
|
||||
for doc in split("\n---", trimspace(file("${local.manifest_dir}/${f}"))) :
|
||||
trimspace(doc)
|
||||
if trimspace(doc) != ""
|
||||
]
|
||||
])
|
||||
|
||||
decoded_documents = [
|
||||
for doc in local.raw_documents : yamldecode(doc)
|
||||
if try(yamldecode(doc).kind, "") != ""
|
||||
]
|
||||
|
||||
cluster_scoped_kinds = toset([
|
||||
"Namespace",
|
||||
"CustomResourceDefinition",
|
||||
"ClusterRole",
|
||||
"ClusterRoleBinding",
|
||||
"ClusterIssuer",
|
||||
"PersistentVolume",
|
||||
"StorageClass",
|
||||
"MutatingWebhookConfiguration",
|
||||
"ValidatingWebhookConfiguration",
|
||||
])
|
||||
|
||||
namespaced_documents = [
|
||||
for m in local.decoded_documents :
|
||||
contains(local.cluster_scoped_kinds, m.kind) ? m : merge(
|
||||
m,
|
||||
{
|
||||
metadata = merge(
|
||||
lookup(m, "metadata", {}),
|
||||
{ namespace = var.namespace }
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
resource "kubernetes_manifest" "namespace" {
|
||||
manifest = {
|
||||
apiVersion = "v1"
|
||||
kind = "Namespace"
|
||||
metadata = { name = var.namespace }
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_manifest" "resources" {
|
||||
for_each = { for idx, m in local.namespaced_documents : tostring(idx) => m }
|
||||
manifest = each.value
|
||||
depends_on = [kubernetes_manifest.namespace]
|
||||
}
|
||||
1
deploy/opentofu/k3s/manifests/.gitkeep
Normal file
1
deploy/opentofu/k3s/manifests/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
|
||||
16
deploy/opentofu/k3s/variables.tf
Normal file
16
deploy/opentofu/k3s/variables.tf
Normal file
@ -0,0 +1,16 @@
|
||||
variable "k3s_server_url" {
|
||||
type = string
|
||||
description = "K3s API server URL (e.g., https://pi.prole.org:6443)"
|
||||
}
|
||||
|
||||
variable "k3s_token" {
|
||||
type = string
|
||||
description = "K3s API token"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "namespace" {
|
||||
type = string
|
||||
description = "Target namespace for Prole resources"
|
||||
default = "default"
|
||||
}
|
||||
@ -36,7 +36,7 @@ export_docker_images() {
|
||||
|
||||
# Collect images from K8s manifests
|
||||
# Robust extraction of image names from YAML files
|
||||
find "${PROJECT_ROOT}/k8s/prole" "${PROJECT_ROOT}/k8s/openbao" -name "*.yaml" -exec grep -h "image:" {} + | awk -F'image:' '{print $2}' | awk '{print $1}' | sed "s/['\"]//g" > /tmp/prole_images.txt
|
||||
find "${PROJECT_ROOT}/k8s/prole" "${PROJECT_ROOT}/k8s/openbao" "${PROJECT_ROOT}/k8s/opentofu" -name "*.yaml" -exec grep -h "image:" {} + | awk -F'image:' '{print $2}' | awk '{print $1}' | sed "s/['\"]//g" > /tmp/prole_images.txt
|
||||
|
||||
# Also check Supabase compose if it exists
|
||||
if [[ -f "${PROJECT_ROOT}/supabase/docker/docker-compose.yml" ]]; then
|
||||
|
||||
@ -11,6 +11,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
# shellcheck disable=SC1090
|
||||
source "$SCRIPT_DIR/prole_cfg.sh"
|
||||
|
||||
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
|
||||
prole_set_mode "${2:-}"
|
||||
shift 2
|
||||
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
|
||||
prole_set_mode "${1#*=}"
|
||||
shift
|
||||
fi
|
||||
|
||||
PROLE_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
|
||||
VAULT_PASS_FILE="$PROLE_ROOT/.vault_pass"
|
||||
|
||||
|
||||
@ -16,6 +16,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
# shellcheck disable=SC1090
|
||||
source "$SCRIPT_DIR/prole_cfg.sh"
|
||||
|
||||
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
|
||||
prole_set_mode "${2:-}"
|
||||
shift 2
|
||||
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
|
||||
prole_set_mode "${1#*=}"
|
||||
shift
|
||||
fi
|
||||
|
||||
ACTION=${1:-start}
|
||||
NAMESPACE_AUTH="authority"
|
||||
KDC_NAME="dog"
|
||||
|
||||
@ -25,6 +25,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
# shellcheck disable=SC1090
|
||||
source "$SCRIPT_DIR/prole_cfg.sh"
|
||||
|
||||
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
|
||||
prole_set_mode "${2:-}"
|
||||
shift 2
|
||||
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
|
||||
prole_set_mode "${1#*=}"
|
||||
shift
|
||||
fi
|
||||
|
||||
if [[ -z "${PROLE_SERVICE:-}" ]]; then
|
||||
echo "ERROR: PROLE_SERVICE is not defined. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
|
||||
exit 1
|
||||
@ -38,15 +46,26 @@ DOMAIN=${DOMAIN:-prole.org}
|
||||
KRB5_KDC=${KRB5_KDC:-}
|
||||
KRB5_ADMIN=${KRB5_ADMIN:-}
|
||||
CNPG_MANIFEST_OVERRIDE=${CNPG_MANIFEST_OVERRIDE:-}
|
||||
PROLE_HOME=${PROLE_HOME:-$(cd "$SCRIPT_DIR/.." && pwd)}
|
||||
BACKUP_DIR=${BACKUP_DIR:-$PROLE_HOME/prole/backup}
|
||||
BACKUP_WAIT_TIMEOUT=${BACKUP_WAIT_TIMEOUT:-1800}
|
||||
RECOVERY_TEMPLATE="$SCRIPT_DIR/../k8s/prole/prole-db-recovery.yaml.tpl"
|
||||
CNPG_FORCE_ROLLOUT_SCRIPT="$SCRIPT_DIR/init_prole-db.sh"
|
||||
BARMAN_PLUGIN_MANIFEST_URL=${BARMAN_PLUGIN_MANIFEST_URL:-}
|
||||
BARMAN_PLUGIN_FALLBACK_VERSION=${BARMAN_PLUGIN_FALLBACK_VERSION:-0.9.0}
|
||||
CERT_MANAGER_MANIFEST_URL=${CERT_MANAGER_MANIFEST_URL:-}
|
||||
CERT_MANAGER_FALLBACK_VERSION=${CERT_MANAGER_FALLBACK_VERSION:-1.19.3}
|
||||
|
||||
# Support both PROLE_HOME/k8s and sibling k8s directory
|
||||
if [[ -d "$SCRIPT_DIR/../k8s/prole" ]]; then
|
||||
CNPG_MANIFEST="$SCRIPT_DIR/../k8s/prole/prole-db.yaml"
|
||||
K8S_PROLE_DIR="$SCRIPT_DIR/../k8s/prole"
|
||||
elif [[ -n "${PROLE_HOME:-}" && -d "$PROLE_HOME/k8s/prole" ]]; then
|
||||
CNPG_MANIFEST="$PROLE_HOME/k8s/prole/prole-db.yaml"
|
||||
K8S_PROLE_DIR="$PROLE_HOME/k8s/prole"
|
||||
else
|
||||
CNPG_MANIFEST="$SCRIPT_DIR/../k8s/prole/prole-db.yaml"
|
||||
K8S_PROLE_DIR="$SCRIPT_DIR/../k8s/prole"
|
||||
fi
|
||||
CNPG_MANIFEST="$K8S_PROLE_DIR/prole-db.yaml"
|
||||
BARMAN_OBJECTSTORE_MANIFEST="$K8S_PROLE_DIR/prole-db-barman-objectstore.yaml"
|
||||
|
||||
SECRETS_DIR="$PROLE_SERVICE/secrets"
|
||||
# Resolving CNPG admin keys.
|
||||
@ -146,6 +165,8 @@ ensure_grafana_admin_secret() {
|
||||
|
||||
ensure_cnpg_operator() {
|
||||
if kubectl get deployment -n cnpg-system cnpg-controller-manager >/dev/null 2>&1; then
|
||||
kubectl -n cnpg-system rollout status deploy/cnpg-controller-manager --timeout=180s || true
|
||||
wait_for_cnpg_webhook 180 || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
@ -158,7 +179,154 @@ ensure_cnpg_operator() {
|
||||
kubectl apply --server-side -f "$yaml_url"
|
||||
|
||||
if kubectl get deployment -n cnpg-system cnpg-controller-manager >/dev/null 2>&1; then
|
||||
kubectl -n cnpg-system rollout status deploy/cnpg-controller-manager --timeout=120s || true
|
||||
kubectl -n cnpg-system rollout status deploy/cnpg-controller-manager --timeout=180s || true
|
||||
wait_for_cnpg_webhook 180 || true
|
||||
fi
|
||||
}
|
||||
|
||||
get_latest_barman_plugin_version() {
|
||||
local version tag
|
||||
tag=$(curl -s "https://api.github.com/repos/cloudnative-pg/plugin-barman-cloud/releases/latest" | jq -r '.tag_name' || echo "")
|
||||
if [[ -z "$tag" || "$tag" == "null" ]]; then
|
||||
echo "v${BARMAN_PLUGIN_FALLBACK_VERSION}"
|
||||
return 0
|
||||
fi
|
||||
echo "$tag"
|
||||
}
|
||||
|
||||
resolve_barman_plugin_manifest_url() {
|
||||
if [[ -n "$BARMAN_PLUGIN_MANIFEST_URL" ]]; then
|
||||
echo "$BARMAN_PLUGIN_MANIFEST_URL"
|
||||
return 0
|
||||
fi
|
||||
local tag
|
||||
tag=$(get_latest_barman_plugin_version)
|
||||
echo "https://github.com/cloudnative-pg/plugin-barman-cloud/releases/download/${tag}/manifest.yaml"
|
||||
}
|
||||
|
||||
cert_manager_ready() {
|
||||
kubectl get crd certificates.cert-manager.io >/dev/null 2>&1
|
||||
}
|
||||
|
||||
get_latest_cert_manager_version() {
|
||||
local tag
|
||||
tag=$(curl -s "https://api.github.com/repos/cert-manager/cert-manager/releases/latest" | jq -r '.tag_name' || echo "")
|
||||
if [[ -z "$tag" || "$tag" == "null" ]]; then
|
||||
echo "v${CERT_MANAGER_FALLBACK_VERSION}"
|
||||
return 0
|
||||
fi
|
||||
echo "$tag"
|
||||
}
|
||||
|
||||
resolve_cert_manager_manifest_url() {
|
||||
if [[ -n "$CERT_MANAGER_MANIFEST_URL" ]]; then
|
||||
echo "$CERT_MANAGER_MANIFEST_URL"
|
||||
return 0
|
||||
fi
|
||||
local tag
|
||||
tag=$(get_latest_cert_manager_version)
|
||||
echo "https://github.com/cert-manager/cert-manager/releases/download/${tag}/cert-manager.yaml"
|
||||
}
|
||||
|
||||
ensure_cert_manager() {
|
||||
if cert_manager_ready; then
|
||||
if kubectl -n cert-manager get deploy cert-manager >/dev/null 2>&1; then
|
||||
kubectl -n cert-manager rollout status deploy/cert-manager --timeout=180s || true
|
||||
kubectl -n cert-manager rollout status deploy/cert-manager-webhook --timeout=180s || true
|
||||
kubectl -n cert-manager rollout status deploy/cert-manager-cainjector --timeout=180s || true
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
local cm_url
|
||||
cm_url=$(resolve_cert_manager_manifest_url)
|
||||
echo "Installing cert-manager from ${cm_url} ..."
|
||||
kubectl apply -f "$cm_url"
|
||||
|
||||
if kubectl -n cert-manager get deploy cert-manager >/dev/null 2>&1; then
|
||||
kubectl -n cert-manager rollout status deploy/cert-manager --timeout=180s || true
|
||||
kubectl -n cert-manager rollout status deploy/cert-manager-webhook --timeout=180s || true
|
||||
kubectl -n cert-manager rollout status deploy/cert-manager-cainjector --timeout=180s || true
|
||||
fi
|
||||
}
|
||||
|
||||
wait_for_barman_crd() {
|
||||
local timeout=${1:-120}
|
||||
local start_time
|
||||
start_time=$(date +%s)
|
||||
while true; do
|
||||
if kubectl get crd objectstores.barmancloud.cnpg.io >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
if (( $(date +%s) - start_time > timeout )); then
|
||||
return 1
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
ensure_barman_plugin() {
|
||||
local crd_present deploy_present
|
||||
crd_present=false
|
||||
deploy_present=false
|
||||
|
||||
if kubectl get crd objectstores.barmancloud.cnpg.io >/dev/null 2>&1; then
|
||||
crd_present=true
|
||||
fi
|
||||
if kubectl -n cnpg-system get deploy barman-cloud >/dev/null 2>&1; then
|
||||
deploy_present=true
|
||||
fi
|
||||
|
||||
if [[ "$crd_present" == "true" && "$deploy_present" == "true" ]]; then
|
||||
kubectl -n cnpg-system rollout status deploy/barman-cloud --timeout=180s || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
ensure_cert_manager
|
||||
|
||||
local plugin_url
|
||||
plugin_url=$(resolve_barman_plugin_manifest_url)
|
||||
echo "Installing Barman Cloud plugin from ${plugin_url} ..."
|
||||
kubectl apply -f "$plugin_url"
|
||||
|
||||
if ! wait_for_barman_crd 120; then
|
||||
echo "WARN: Barman Cloud ObjectStore CRD not ready after install." >&2
|
||||
fi
|
||||
|
||||
if kubectl -n cnpg-system get deploy barman-cloud >/dev/null 2>&1; then
|
||||
kubectl -n cnpg-system rollout status deploy/barman-cloud --timeout=180s || true
|
||||
fi
|
||||
}
|
||||
|
||||
wait_for_cnpg_webhook() {
|
||||
local timeout=${1:-120}
|
||||
local start_time
|
||||
start_time=$(date +%s)
|
||||
|
||||
echo "Waiting for CNPG webhook service endpoints to be ready..."
|
||||
while true; do
|
||||
local endpoints
|
||||
endpoints=$(kubectl -n cnpg-system get endpoints cnpg-webhook-service -o jsonpath='{.subsets[*].addresses[*].ip}' 2>/dev/null || true)
|
||||
if [[ -n "$endpoints" ]]; then
|
||||
echo "CNPG webhook service has endpoints."
|
||||
return 0
|
||||
fi
|
||||
if (( $(date +%s) - start_time > timeout )); then
|
||||
echo "WARN: CNPG webhook endpoints not ready after ${timeout}s."
|
||||
return 1
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
apply_barman_objectstore_if_present() {
|
||||
if [[ ! -f "$BARMAN_OBJECTSTORE_MANIFEST" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if kubectl get crd objectstores.barmancloud.cnpg.io >/dev/null 2>&1; then
|
||||
kubectl apply -n "$NAMESPACE" -f "$BARMAN_OBJECTSTORE_MANIFEST"
|
||||
else
|
||||
echo "WARN: Barman Cloud ObjectStore CRD not found; skipping $BARMAN_OBJECTSTORE_MANIFEST."
|
||||
fi
|
||||
}
|
||||
|
||||
@ -171,27 +339,30 @@ ensure_prole_stack_resources() {
|
||||
return 1
|
||||
fi
|
||||
local dir file
|
||||
dir="$SCRIPT_DIR/../k8s/prole"
|
||||
dir="$K8S_PROLE_DIR"
|
||||
for file in "$dir"/*.yaml; do
|
||||
case "$(basename "$file")" in
|
||||
prole-db.yaml|kustomization.yaml|supabase-*.yaml)
|
||||
prole-db.yaml|kustomization.yaml|supabase-*.yaml|prole-db-barman-objectstore.yaml)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
kubectl apply -n "$NAMESPACE" -f "$file"
|
||||
done
|
||||
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST_OVERRIDE"
|
||||
apply_barman_objectstore_if_present
|
||||
apply_cnpg_cluster_manifest "$CNPG_MANIFEST_OVERRIDE"
|
||||
else
|
||||
local dir file
|
||||
dir="$SCRIPT_DIR/../k8s/prole"
|
||||
dir="$K8S_PROLE_DIR"
|
||||
for file in "$dir"/*.yaml; do
|
||||
case "$(basename "$file")" in
|
||||
kustomization.yaml|supabase-*.yaml)
|
||||
prole-db.yaml|kustomization.yaml|supabase-*.yaml|prole-db-barman-objectstore.yaml)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
kubectl apply -n "$NAMESPACE" -f "$file"
|
||||
done
|
||||
apply_barman_objectstore_if_present
|
||||
apply_cnpg_cluster_manifest "$CNPG_MANIFEST"
|
||||
fi
|
||||
|
||||
# Ensure prole-index-html exists for prole deployment readiness probe
|
||||
@ -213,6 +384,30 @@ ensure_prole_stack_resources() {
|
||||
fi
|
||||
}
|
||||
|
||||
apply_cnpg_cluster_manifest() {
|
||||
local manifest="$1"
|
||||
local attempts=${CNPG_APPLY_RETRIES:-6}
|
||||
local i out
|
||||
|
||||
for ((i=1; i<=attempts; i++)); do
|
||||
if out=$(kubectl apply -n "$NAMESPACE" -f "$manifest" 2>&1); then
|
||||
printf '%s\n' "$out"
|
||||
return 0
|
||||
fi
|
||||
if echo "$out" | grep -q "cnpg-webhook-service"; then
|
||||
echo "CNPG webhook not ready yet (attempt $i/$attempts). Retrying..."
|
||||
sleep 5
|
||||
continue
|
||||
fi
|
||||
echo "$out" >&2
|
||||
return 1
|
||||
done
|
||||
|
||||
echo "ERROR: Failed to apply CNPG manifest after $attempts attempts." >&2
|
||||
echo "$out" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_cnpg_pods() {
|
||||
local timeout=${1:-300}
|
||||
local start_time
|
||||
@ -261,6 +456,216 @@ wait_for_cnpg_pods() {
|
||||
done
|
||||
}
|
||||
|
||||
cluster_exists() {
|
||||
kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
cluster_has_pods() {
|
||||
kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" --no-headers 2>/dev/null | grep -q .
|
||||
}
|
||||
|
||||
latest_backup_name() {
|
||||
kubectl -n "$NAMESPACE" get backup \
|
||||
--sort-by=.metadata.creationTimestamp \
|
||||
-o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null | tail -n 1
|
||||
}
|
||||
|
||||
latest_completed_backup_name() {
|
||||
kubectl -n "$NAMESPACE" get backup \
|
||||
--sort-by=.metadata.creationTimestamp \
|
||||
-o jsonpath='{range .items[*]}{.metadata.name}{"|"}{.status.phase}{"\n"}{end}' 2>/dev/null | \
|
||||
awk -F'|' '{p=tolower($2); if (p=="completed" || p=="succeeded") {name=$1}} END {print name}'
|
||||
}
|
||||
|
||||
wait_for_backup() {
|
||||
local backup_name="$1"
|
||||
local start_time now phase phase_lc
|
||||
start_time=$(date +%s)
|
||||
|
||||
while true; do
|
||||
phase=$(kubectl -n "$NAMESPACE" get backup "$backup_name" -o jsonpath='{.status.phase}' 2>/dev/null || true)
|
||||
phase_lc=$(printf '%s' "$phase" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
case "$phase_lc" in
|
||||
completed|succeeded)
|
||||
echo "Backup $backup_name completed."
|
||||
return 0
|
||||
;;
|
||||
failed|error)
|
||||
echo "Backup $backup_name failed (phase=$phase)." >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
now=$(date +%s)
|
||||
if (( now - start_time > BACKUP_WAIT_TIMEOUT )); then
|
||||
echo "Timed out waiting for backup $backup_name." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Waiting for backup $backup_name to complete (phase=${phase:-unknown}) ..."
|
||||
sleep 10
|
||||
done
|
||||
}
|
||||
|
||||
run_garage_backup() {
|
||||
if [[ ! -x "$SCRIPT_DIR/init_prole-db-backup.sh" ]]; then
|
||||
echo "WARN: init_prole-db-backup.sh not found; skipping Garage backup." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Running Garage backup via init_prole-db-backup.sh ..."
|
||||
if ! "$SCRIPT_DIR/init_prole-db-backup.sh" start; then
|
||||
echo "Garage backup script failed." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
local backup_name
|
||||
backup_name=$(latest_backup_name)
|
||||
if [[ -z "$backup_name" ]]; then
|
||||
echo "No backup resource detected after triggering backup." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
wait_for_backup "$backup_name"
|
||||
}
|
||||
|
||||
get_primary_pod() {
|
||||
local primary
|
||||
primary=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.status.currentPrimary}' 2>/dev/null || true)
|
||||
if [[ -z "$primary" ]]; then
|
||||
primary=$(kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
||||
fi
|
||||
printf '%s' "$primary"
|
||||
}
|
||||
|
||||
decode_b64() {
|
||||
local data="$1"
|
||||
if [[ -z "$data" ]]; then
|
||||
return 1
|
||||
fi
|
||||
printf '%s' "$data" | base64 -d 2>/dev/null
|
||||
}
|
||||
|
||||
resolve_db_credentials() {
|
||||
local user_b64 pass_b64 user pass
|
||||
|
||||
user_b64=$(kubectl -n "$NAMESPACE" get secret prole-db-user -o jsonpath='{.data.username}' 2>/dev/null || true)
|
||||
pass_b64=$(kubectl -n "$NAMESPACE" get secret prole-db-user -o jsonpath='{.data.password}' 2>/dev/null || true)
|
||||
user=$(decode_b64 "$user_b64" || true)
|
||||
pass=$(decode_b64 "$pass_b64" || true)
|
||||
|
||||
if [[ -z "$user" || -z "$pass" ]]; then
|
||||
user_b64=$(kubectl -n "$NAMESPACE" get secret prole-db-superuser -o jsonpath='{.data.username}' 2>/dev/null || true)
|
||||
pass_b64=$(kubectl -n "$NAMESPACE" get secret prole-db-superuser -o jsonpath='{.data.password}' 2>/dev/null || true)
|
||||
user=$(decode_b64 "$user_b64" || true)
|
||||
pass=$(decode_b64 "$pass_b64" || true)
|
||||
fi
|
||||
|
||||
if [[ -z "$user" || -z "$pass" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s\n%s' "$user" "$pass"
|
||||
}
|
||||
|
||||
pgdump_local() {
|
||||
local pod user pass db_name dump_dir dump_file timestamp
|
||||
pod=$(get_primary_pod)
|
||||
if [[ -z "$pod" ]]; then
|
||||
echo "ERROR: No CNPG pod available for pg_dump." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local creds
|
||||
if ! creds=$(resolve_db_credentials); then
|
||||
echo "ERROR: Unable to resolve database credentials for pg_dump." >&2
|
||||
return 1
|
||||
fi
|
||||
user=$(printf '%s' "$creds" | sed -n '1p')
|
||||
pass=$(printf '%s' "$creds" | sed -n '2p')
|
||||
|
||||
db_name=${PROLE_DB_NAME:-prole-db}
|
||||
dump_dir="$BACKUP_DIR"
|
||||
mkdir -p "$dump_dir"
|
||||
|
||||
timestamp=$(date +%Y%m%d%H%M%S)
|
||||
dump_file="$dump_dir/${CNPG_CLUSTER_NAME}-pgdump-${timestamp}.dump"
|
||||
|
||||
echo "Running pg_dump against pod $pod (db=$db_name) ..."
|
||||
if kubectl -n "$NAMESPACE" exec "$pod" -c postgres -- env PGPASSWORD="$pass" \
|
||||
pg_dump -U "$user" -d "$db_name" -Fc > "$dump_file"; then
|
||||
echo "pg_dump saved to $dump_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "pg_dump failed; removing partial file." >&2
|
||||
rm -f "$dump_file"
|
||||
return 1
|
||||
}
|
||||
|
||||
attempt_backup_if_active() {
|
||||
if ! cluster_exists; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! cluster_has_pods; then
|
||||
echo "CNPG cluster '$CNPG_CLUSTER_NAME' exists but no pods detected; skipping backup." >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "CNPG cluster '$CNPG_CLUSTER_NAME' detected; attempting Garage backup ..."
|
||||
if run_garage_backup; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Garage backup failed; attempting local pg_dump ..." >&2
|
||||
if pgdump_local; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "WARN: Both Garage backup and local pg_dump failed." >&2
|
||||
return 0
|
||||
}
|
||||
|
||||
force_rollout() {
|
||||
if [[ ! -x "$CNPG_FORCE_ROLLOUT_SCRIPT" ]]; then
|
||||
echo "WARN: Force rollout script not found at $CNPG_FORCE_ROLLOUT_SCRIPT" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Attempting force rollout of CNPG pods ..."
|
||||
"$CNPG_FORCE_ROLLOUT_SCRIPT" rollout
|
||||
}
|
||||
|
||||
reset_and_reinit() {
|
||||
local backup_name tmp_manifest
|
||||
backup_name=$(latest_completed_backup_name)
|
||||
|
||||
echo "Resetting CNPG cluster '$CNPG_CLUSTER_NAME' in namespace '$NAMESPACE' ..."
|
||||
kubectl -n "$NAMESPACE" delete cluster "$CNPG_CLUSTER_NAME" --ignore-not-found
|
||||
kubectl -n "$NAMESPACE" wait --for=delete pod -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" --timeout=180s >/dev/null 2>&1 || true
|
||||
|
||||
if [[ -n "$backup_name" && -f "$RECOVERY_TEMPLATE" ]]; then
|
||||
tmp_manifest=$(mktemp)
|
||||
sed "s/{{BACKUP_NAME}}/${backup_name}/g" "$RECOVERY_TEMPLATE" > "$tmp_manifest"
|
||||
echo "Re-initializing from backup $backup_name ..."
|
||||
CNPG_MANIFEST_OVERRIDE="$tmp_manifest" ensure_prole_stack_resources
|
||||
rm -f "$tmp_manifest"
|
||||
else
|
||||
if [[ -n "$backup_name" && ! -f "$RECOVERY_TEMPLATE" ]]; then
|
||||
echo "WARN: Recovery template not found: $RECOVERY_TEMPLATE" >&2
|
||||
fi
|
||||
if [[ -z "$backup_name" ]]; then
|
||||
echo "No completed backups found; starting fresh initialization." >&2
|
||||
fi
|
||||
ensure_prole_stack_resources
|
||||
fi
|
||||
|
||||
wait_for_cnpg_pods 300
|
||||
}
|
||||
|
||||
# Resolve OpenBao URL: prefer explicit env, then localhost port-forward, then cluster DNS
|
||||
bao_service_url() {
|
||||
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
|
||||
@ -378,7 +783,9 @@ get_latest_cnpg_version() {
|
||||
initialize() {
|
||||
ensure_tools
|
||||
ensure_namespace
|
||||
attempt_backup_if_active
|
||||
ensure_cnpg_operator
|
||||
ensure_barman_plugin
|
||||
echo "Using namespace: $NAMESPACE"
|
||||
|
||||
fetch_admin_keys_and_db_pass_from_bao_or_local
|
||||
@ -387,7 +794,20 @@ initialize() {
|
||||
ensure_prole_stack_resources
|
||||
|
||||
if ! wait_for_cnpg_pods 300; then
|
||||
return 1
|
||||
echo "WARN: CNPG pods did not become ready after init; attempting force rollout ..." >&2
|
||||
if force_rollout; then
|
||||
if ! wait_for_cnpg_pods 300; then
|
||||
echo "WARN: Force rollout did not recover CNPG; attempting reset and re-init ..." >&2
|
||||
if ! reset_and_reinit; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "WARN: Force rollout failed; attempting reset and re-init ..." >&2
|
||||
if ! reset_and_reinit; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Support both names for the toggle from prole.cfg
|
||||
@ -438,6 +858,7 @@ case "$ACTION" in
|
||||
ensure_tools
|
||||
ensure_namespace
|
||||
ensure_cnpg_operator
|
||||
ensure_barman_plugin
|
||||
if [[ ! -f "$CNPG_MANIFEST" ]]; then
|
||||
echo "ERROR: CNPG manifest not found at $CNPG_MANIFEST" >&2
|
||||
exit 1
|
||||
@ -485,8 +906,13 @@ case "$ACTION" in
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
install-barman-plugin)
|
||||
ensure_tools
|
||||
ensure_cnpg_operator
|
||||
ensure_barman_plugin
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {create|delete|recreate|start|stop|status|restart|initialize|update|reload} [dbname]" >&2
|
||||
echo "Usage: $0 {create|delete|recreate|start|stop|status|restart|initialize|update|reload|install-barman-plugin} [dbname]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
@ -14,6 +14,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
# shellcheck disable=SC1090
|
||||
source "$SCRIPT_DIR/prole_cfg.sh"
|
||||
|
||||
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
|
||||
prole_set_mode "${2:-}"
|
||||
shift 2
|
||||
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
|
||||
prole_set_mode "${1#*=}"
|
||||
shift
|
||||
fi
|
||||
|
||||
ACTION=${1:-}
|
||||
|
||||
if [[ -n "${GARAGE_INIT_LOG:-}" ]]; then
|
||||
|
||||
@ -32,6 +32,7 @@ Actions:
|
||||
|
||||
Options:
|
||||
-v, --verbose Enable verbose output
|
||||
-m, --mode k3d|k3s|k8s (maps to dev|service|prod)
|
||||
-e, --environment dev|service|prod (default: $ENVIRONMENT)
|
||||
-h, --host Remote host for service/prod environments
|
||||
-n, --name k3d cluster name
|
||||
@ -58,6 +59,24 @@ while [[ $# -gt 0 ]]; do
|
||||
VERBOSE=true
|
||||
shift
|
||||
;;
|
||||
-m|--mode)
|
||||
prole_set_mode "${2:-}"
|
||||
case "${PROLE_MODE:-}" in
|
||||
k3d) ENVIRONMENT="dev" ;;
|
||||
k3s) ENVIRONMENT="service" ;;
|
||||
k8s) ENVIRONMENT="prod" ;;
|
||||
esac
|
||||
shift 2
|
||||
;;
|
||||
--mode=*|-m=*)
|
||||
prole_set_mode "${1#*=}"
|
||||
case "${PROLE_MODE:-}" in
|
||||
k3d) ENVIRONMENT="dev" ;;
|
||||
k3s) ENVIRONMENT="service" ;;
|
||||
k8s) ENVIRONMENT="prod" ;;
|
||||
esac
|
||||
shift
|
||||
;;
|
||||
-e|--environment)
|
||||
ENVIRONMENT="$2"
|
||||
shift 2
|
||||
@ -85,12 +104,19 @@ if [[ -z "$ACTION" ]]; then
|
||||
fi
|
||||
|
||||
ensure_tools() {
|
||||
for t in k3d docker; do
|
||||
command -v "$t" >/dev/null || { echo "ERROR: Missing required tool: $t" >&2; exit 1; }
|
||||
done
|
||||
if [[ "$ENVIRONMENT" == "dev" ]]; then
|
||||
for t in k3d docker; do
|
||||
command -v "$t" >/dev/null || { echo "ERROR: Missing required tool: $t" >&2; exit 1; }
|
||||
done
|
||||
else
|
||||
command -v kubectl >/dev/null || { echo "ERROR: Missing required tool: kubectl" >&2; exit 1; }
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_docker() {
|
||||
if [[ "$ENVIRONMENT" != "dev" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "ERROR: Docker is not running." >&2
|
||||
exit 1
|
||||
|
||||
@ -20,6 +20,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
# shellcheck disable=SC1090
|
||||
source "$SCRIPT_DIR/prole_cfg.sh"
|
||||
|
||||
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
|
||||
prole_set_mode "${2:-}"
|
||||
shift 2
|
||||
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
|
||||
prole_set_mode "${1#*=}"
|
||||
shift
|
||||
fi
|
||||
|
||||
ACTION=${1:-initialize}
|
||||
|
||||
CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db}
|
||||
@ -49,6 +57,11 @@ ensure_tools() {
|
||||
done
|
||||
}
|
||||
|
||||
cnpg_supports_pod_template() {
|
||||
kubectl get crd clusters.postgresql.cnpg.io -o json 2>/dev/null \
|
||||
| jq -e '.spec.versions[] | select(.name=="v1") | .schema.openAPIV3Schema.properties.spec.properties.podTemplate' >/dev/null
|
||||
}
|
||||
|
||||
sha256_stdin() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum | awk '{print $1}'
|
||||
@ -288,6 +301,10 @@ apply_krb5_conf_mount_to_cnpg() {
|
||||
err "ERROR: Cluster '$CNPG_CLUSTER_NAME' not found in namespace '$NAMESPACE'."
|
||||
return 1
|
||||
fi
|
||||
if ! cnpg_supports_pod_template; then
|
||||
err "CNPG CRD does not support spec.podTemplate; falling back to in-pod krb5.conf update."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local conf conf_hash
|
||||
conf=$(get_krb5_conf)
|
||||
|
||||
@ -10,23 +10,35 @@ set -euo pipefail
|
||||
# Initialize SCRIPT_DIR
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
|
||||
ACTION=${1:-test}
|
||||
ORIG_ARGS=("$@")
|
||||
|
||||
# Load env
|
||||
if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then
|
||||
set --
|
||||
# shellcheck disable=SC1090
|
||||
source "$PROLE_HOME/env.sh"
|
||||
set -- "${ORIG_ARGS[@]}"
|
||||
elif [[ -f "$HOME/.prole/env.sh" ]]; then
|
||||
set --
|
||||
# shellcheck disable=SC1090
|
||||
source "$HOME/.prole/env.sh"
|
||||
set -- "${ORIG_ARGS[@]}"
|
||||
fi
|
||||
|
||||
# Load config values from prole.cfg before applying defaults/CLI overrides.
|
||||
# shellcheck disable=SC1090
|
||||
source "$SCRIPT_DIR/prole_cfg.sh"
|
||||
|
||||
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
|
||||
prole_set_mode "${2:-}"
|
||||
shift 2
|
||||
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
|
||||
prole_set_mode "${1#*=}"
|
||||
shift
|
||||
fi
|
||||
|
||||
ACTION=${1:-test}
|
||||
|
||||
NAMESPACE=${NAMESPACE:-default}
|
||||
KRB5_REALM=${KRB5_REALM:-${REALM:-}}
|
||||
KRB5_KDC=${KRB5_KDC:-}
|
||||
|
||||
@ -13,6 +13,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
# shellcheck disable=SC1090
|
||||
source "$SCRIPT_DIR/prole_cfg.sh"
|
||||
|
||||
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
|
||||
prole_set_mode "${2:-}"
|
||||
shift 2
|
||||
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
|
||||
prole_set_mode "${1#*=}"
|
||||
shift
|
||||
fi
|
||||
|
||||
if [[ -z "${PROLE_SERVICE:-}" ]]; then
|
||||
echo "ERROR: PROLE_SERVICE is not defined. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
|
||||
exit 1
|
||||
|
||||
@ -51,6 +51,13 @@ root_token_file="$SECRETS_DIR/openbao-root-token"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-m|--mode)
|
||||
shift
|
||||
prole_set_mode "${1:-}"
|
||||
;;
|
||||
-m=*|--mode=*)
|
||||
prole_set_mode "${1#*=}"
|
||||
;;
|
||||
-n|--namespace)
|
||||
shift
|
||||
if [[ -z "${1:-}" ]]; then
|
||||
|
||||
207
etc/init_opentofu.sh
Executable file
207
etc/init_opentofu.sh
Executable file
@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# init_opentofu.sh
|
||||
# Purpose:
|
||||
# - Deploy OpenTofu control plane into Kubernetes
|
||||
# - Configure admin access using the Prole DB root password
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
|
||||
# Load environment and config via prole_cfg.sh
|
||||
# shellcheck disable=SC1090
|
||||
source "$SCRIPT_DIR/prole_cfg.sh"
|
||||
|
||||
if [[ -z "${PROLE_SERVICE:-}" ]]; then
|
||||
echo "ERROR: PROLE_SERVICE is not defined. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ACTION=""
|
||||
NAMESPACE_OVERRIDE=""
|
||||
|
||||
OPENTOFU_NAME=${OPENTOFU_NAME:-opentofu}
|
||||
OPENTOFU_ADMIN_USER=${OPENTOFU_ADMIN_USER:-admin}
|
||||
|
||||
if [[ -d "$SCRIPT_DIR/../k8s/opentofu" ]]; then
|
||||
OPENTOFU_MANIFEST_DIR="$SCRIPT_DIR/../k8s/opentofu"
|
||||
elif [[ -n "${PROLE_HOME:-}" && -d "$PROLE_HOME/k8s/opentofu" ]]; then
|
||||
OPENTOFU_MANIFEST_DIR="$PROLE_HOME/k8s/opentofu"
|
||||
else
|
||||
OPENTOFU_MANIFEST_DIR="$SCRIPT_DIR/../k8s/opentofu"
|
||||
fi
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-m|--mode)
|
||||
shift
|
||||
prole_set_mode "${1:-}"
|
||||
;;
|
||||
-m=*|--mode=*)
|
||||
prole_set_mode "${1#*=}"
|
||||
;;
|
||||
-n|--namespace)
|
||||
shift
|
||||
if [[ -z "${1:-}" ]]; then
|
||||
echo "ERROR: -n/--namespace requires a value" >&2
|
||||
exit 2
|
||||
fi
|
||||
NAMESPACE_OVERRIDE="$1"
|
||||
;;
|
||||
-n=*|--namespace=*)
|
||||
NAMESPACE_OVERRIDE="${1#*=}"
|
||||
;;
|
||||
start|stop|status|restart|initialize|update|reload)
|
||||
ACTION="$1"
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$ACTION" && "$1" != -* ]]; then
|
||||
ACTION="$1"
|
||||
else
|
||||
echo "Unknown argument: $1" >&2
|
||||
exit 2
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [[ -n "$NAMESPACE_OVERRIDE" ]]; then
|
||||
NAMESPACE="$NAMESPACE_OVERRIDE"
|
||||
export NAMESPACE
|
||||
fi
|
||||
|
||||
if [[ -z "${NAMESPACE:-}" ]]; then
|
||||
NAMESPACE="default"
|
||||
export NAMESPACE
|
||||
fi
|
||||
|
||||
ensure_tools() {
|
||||
for t in kubectl openssl curl jq; do
|
||||
command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; }
|
||||
done
|
||||
}
|
||||
|
||||
ensure_namespace() {
|
||||
if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then
|
||||
echo "Creating namespace '$NAMESPACE' ..."
|
||||
kubectl create namespace "$NAMESPACE" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
openbao_url() {
|
||||
if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then
|
||||
echo "$PROLE_OPENBAO_URL"
|
||||
elif curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then
|
||||
echo "http://127.0.0.1:18200"
|
||||
else
|
||||
echo "http://openbao.${NAMESPACE}.svc.cluster.local:8200"
|
||||
fi
|
||||
}
|
||||
|
||||
openbao_token() {
|
||||
if [[ -f "$PROLE_SERVICE/secrets/openbao-root-token" ]]; then
|
||||
cat "$PROLE_SERVICE/secrets/openbao-root-token"
|
||||
else
|
||||
echo "${OPENBAO_ROOT_TOKEN:-}"
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_openbao_secret() {
|
||||
local path="$1"
|
||||
local key="$2"
|
||||
local token url
|
||||
token=$(openbao_token)
|
||||
url=$(openbao_url)
|
||||
if [[ -z "$token" ]]; then
|
||||
echo ""
|
||||
return 0
|
||||
fi
|
||||
curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/$path" | jq -r ".data.data.\"$key\"" || echo ""
|
||||
}
|
||||
|
||||
is_anchor() {
|
||||
case "${1:-}" in
|
||||
'${OPENBAO:'*|'${PROLE_SECRET:'*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
resolve_admin_password() {
|
||||
if [[ -z "${OPENTOFU_ADMIN_PASSWORD:-}" ]]; then
|
||||
OPENTOFU_ADMIN_PASSWORD="${DB_PASSWORD:-}"
|
||||
fi
|
||||
|
||||
if [[ -z "${OPENTOFU_ADMIN_PASSWORD:-}" ]] || is_anchor "${OPENTOFU_ADMIN_PASSWORD:-}"; then
|
||||
local fetched
|
||||
fetched=$(fetch_openbao_secret "prole/${NAMESPACE:-default}/db" "password")
|
||||
if [[ -n "$fetched" && "$fetched" != "null" ]]; then
|
||||
OPENTOFU_ADMIN_PASSWORD="$fetched"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${OPENTOFU_ADMIN_PASSWORD:-}" ]]; then
|
||||
echo "ERROR: OPENTOFU_ADMIN_PASSWORD/DB_PASSWORD is missing." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_opentofu_secret() {
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
local hash
|
||||
hash=$(printf "%s" "$OPENTOFU_ADMIN_PASSWORD" | openssl passwd -apr1 -stdin)
|
||||
printf "%s:%s\n" "$OPENTOFU_ADMIN_USER" "$hash" >"$tmp"
|
||||
|
||||
kubectl create secret generic opentofu-admin \
|
||||
-n "$NAMESPACE" \
|
||||
--from-literal=admin_password="$OPENTOFU_ADMIN_PASSWORD" \
|
||||
--from-file=auth="$tmp" \
|
||||
--dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
||||
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
apply_k8s() {
|
||||
echo "Applying OpenTofu manifest to namespace '$NAMESPACE' ..."
|
||||
kubectl apply -n "$NAMESPACE" -f "$OPENTOFU_MANIFEST_DIR/deployment.yaml"
|
||||
kubectl rollout status deploy/$OPENTOFU_NAME -n "$NAMESPACE" --timeout=120s || true
|
||||
}
|
||||
|
||||
delete_k8s() {
|
||||
echo "Removing OpenTofu resources from namespace '$NAMESPACE' ..."
|
||||
kubectl delete -n "$NAMESPACE" -f "$OPENTOFU_MANIFEST_DIR/deployment.yaml" --ignore-not-found
|
||||
kubectl delete -n "$NAMESPACE" secret opentofu-admin --ignore-not-found || true
|
||||
}
|
||||
|
||||
status_k8s() {
|
||||
kubectl -n "$NAMESPACE" get deploy "$OPENTOFU_NAME" 2>/dev/null || true
|
||||
kubectl -n "$NAMESPACE" get svc "$OPENTOFU_NAME" 2>/dev/null || true
|
||||
}
|
||||
|
||||
case "${ACTION:-}" in
|
||||
start|initialize|update|reload|restart)
|
||||
ensure_tools
|
||||
ensure_namespace
|
||||
resolve_admin_password
|
||||
ensure_opentofu_secret
|
||||
apply_k8s
|
||||
;;
|
||||
stop)
|
||||
ensure_tools
|
||||
delete_k8s
|
||||
;;
|
||||
status)
|
||||
ensure_tools
|
||||
status_k8s
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [start|stop|status|restart|initialize|update|reload] [-n namespace] [--mode k3d|k3s|k8s]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@ -131,11 +131,16 @@ if [ -z "${PF_MANAGEMENT_NAMESPACE:-}" ] && [ -n "${NAMESPACE:-}" ]; then
|
||||
PF_MANAGEMENT_NAMESPACE="$NAMESPACE"
|
||||
fi
|
||||
|
||||
# SUPABASE_NAMESPACE="${SUPABASE_NAMESPACE:-supabase}"
|
||||
# SUPABASE_DB_TARGET="${SUPABASE_DB_TARGET:-${SUPABASE_DB_SERVICE:-svc/db}}"
|
||||
SUPABASE_NAMESPACE="${SUPABASE_NAMESPACE:-supabase}"
|
||||
SUPABASE_DB_SERVICE="${SUPABASE_DB_SERVICE:-db}"
|
||||
SUPABASE_DB_TARGET="${SUPABASE_DB_TARGET:-svc/$SUPABASE_DB_SERVICE}"
|
||||
|
||||
# Supabase logic removed as it's handled in another script
|
||||
SUPABASE_ENABLED_EFFECTIVE=0
|
||||
if is_truthy "${SUPABASE_ENABLED:-}"; then
|
||||
SUPABASE_ENABLED_EFFECTIVE=1
|
||||
elif is_truthy "${init_cluster_supabase_enabled:-}"; then
|
||||
SUPABASE_ENABLED_EFFECTIVE=1
|
||||
fi
|
||||
|
||||
PROLE_DB_ALT_PORT_BASE="${PROLE_DB_ALT_PORT_BASE:-15432}"
|
||||
PROLE_DB_ALT_PORT_EFFECTIVE=""
|
||||
@ -212,6 +217,20 @@ validate_env() {
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_supabase_ports() {
|
||||
[ "$SUPABASE_ENABLED_EFFECTIVE" -eq 1 ] || return 0
|
||||
|
||||
local wire_script="$SCRIPT_DIR/init_supabase_ports.sh"
|
||||
if [ ! -x "$wire_script" ]; then
|
||||
warn "Supabase port wiring script not found/executable: $wire_script"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Ensuring Supabase DB wiring (db -> prole-db-rw, supabase-postgres -> 15432) ..."
|
||||
SUPABASE_NAMESPACE="$SUPABASE_NAMESPACE" \
|
||||
"$wire_script" -n "$NAMESPACE" --supabase-namespace "$SUPABASE_NAMESPACE"
|
||||
}
|
||||
|
||||
# ---- Preflight: Docker + k3d awareness ----
|
||||
# Return 0 when Docker CLI can talk to a running daemon.
|
||||
docker_is_running() {
|
||||
@ -287,6 +306,13 @@ ensure_k3d_ready_if_applicable() {
|
||||
fi
|
||||
}
|
||||
|
||||
is_local_mode() {
|
||||
case "${PROLE_MODE:-}" in
|
||||
""|k3d) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
pid_file_for() { printf '%s/%s.pid' "$PID_DIR" "$1"; }
|
||||
log_file_for() { printf '%s/%s.log' "$LOG_DIR" "$1"; }
|
||||
|
||||
@ -418,7 +444,10 @@ foreach_mapping() {
|
||||
if [ -n "${DB_HOST_PORT:-}" ]; then
|
||||
hostPort="$DB_HOST_PORT"
|
||||
fi
|
||||
# Supabase override removed
|
||||
if [ "$SUPABASE_ENABLED_EFFECTIVE" -eq 1 ]; then
|
||||
ns="$SUPABASE_NAMESPACE"
|
||||
target="$SUPABASE_DB_TARGET"
|
||||
fi
|
||||
fi
|
||||
|
||||
# prole-db logic removed, now uses XML
|
||||
@ -602,9 +631,12 @@ scan_for_collisions() {
|
||||
|
||||
do_start() {
|
||||
validate_env
|
||||
# Preflight: require Docker daemon and k3d (if applicable)
|
||||
ensure_docker_running
|
||||
ensure_k3d_ready_if_applicable
|
||||
if is_local_mode; then
|
||||
# Preflight: require Docker daemon and k3d (if applicable)
|
||||
ensure_docker_running
|
||||
ensure_k3d_ready_if_applicable
|
||||
fi
|
||||
ensure_supabase_ports
|
||||
|
||||
vlog "Scanning for port collisions..."
|
||||
foreach_mapping scan_for_collisions
|
||||
@ -620,9 +652,12 @@ do_stop() {
|
||||
|
||||
do_restart() {
|
||||
validate_env
|
||||
# Preflight: require Docker daemon and k3d (if applicable)
|
||||
ensure_docker_running
|
||||
ensure_k3d_ready_if_applicable
|
||||
if is_local_mode; then
|
||||
# Preflight: require Docker daemon and k3d (if applicable)
|
||||
ensure_docker_running
|
||||
ensure_k3d_ready_if_applicable
|
||||
fi
|
||||
ensure_supabase_ports
|
||||
|
||||
vlog "Scanning for port collisions (excluding our own)..."
|
||||
# During restart, we'll stop them first anyway, but let's be safe.
|
||||
@ -702,6 +737,15 @@ while [ $# -gt 0 ]; do
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
-m|--mode)
|
||||
shift
|
||||
prole_set_mode "${1:-}"
|
||||
shift
|
||||
;;
|
||||
-m=*|--mode=*)
|
||||
prole_set_mode "${1#*=}"
|
||||
shift
|
||||
;;
|
||||
-v|--verbose)
|
||||
VERBOSE=1
|
||||
shift
|
||||
|
||||
@ -4,7 +4,7 @@ set -euo pipefail
|
||||
|
||||
# init_prole-db-backup.sh
|
||||
# Purpose:
|
||||
# - Configure CloudNative-PG to backup to Garage (S3-compatible)
|
||||
# - Configure CloudNative-PG to backup to Garage (S3-compatible) via Barman Cloud Plugin
|
||||
# - Create initial backup
|
||||
|
||||
# Initialize SCRIPT_DIR
|
||||
@ -14,6 +14,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
# shellcheck disable=SC1090
|
||||
source "$SCRIPT_DIR/prole_cfg.sh"
|
||||
|
||||
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
|
||||
prole_set_mode "${2:-}"
|
||||
shift 2
|
||||
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
|
||||
prole_set_mode "${1#*=}"
|
||||
shift
|
||||
fi
|
||||
|
||||
ACTION=${1:-start}
|
||||
|
||||
NAMESPACE=${NAMESPACE:-default}
|
||||
@ -25,6 +33,9 @@ GARAGE_BACKUP_SECRET_NAME=${GARAGE_BACKUP_SECRET_NAME:-prole-db-barman-s3}
|
||||
GARAGE_S3_ENDPOINT=${GARAGE_S3_ENDPOINT:-http://$GARAGE_NAME.$NAMESPACE.svc.cluster.local:3900}
|
||||
GARAGE_S3_REGION=${GARAGE_S3_REGION:-garage}
|
||||
RUN_FIRST_BACKUP=${RUN_FIRST_BACKUP:-1}
|
||||
RETENTION_POLICY=${RETENTION_POLICY:-30d}
|
||||
BARMAN_PLUGIN_NAME=${BARMAN_PLUGIN_NAME:-barman-cloud.cloudnative-pg.io}
|
||||
BARMAN_OBJECT_NAME=${BARMAN_OBJECT_NAME:-prole-db-barman-objectstore}
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
@ -58,6 +69,10 @@ ensure_cluster() {
|
||||
fi
|
||||
}
|
||||
|
||||
barman_crd_ready() {
|
||||
kubectl get crd objectstores.barmancloud.cnpg.io >/dev/null 2>&1
|
||||
}
|
||||
|
||||
get_garage_pod() {
|
||||
kubectl get pods -n "$NAMESPACE" -l "app=$GARAGE_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true
|
||||
}
|
||||
@ -137,26 +152,82 @@ ensure_garage_bucket_and_key() {
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
}
|
||||
|
||||
configure_cnpg_backup() {
|
||||
echo "Configuring CNPG backup to use Garage bucket '$GARAGE_BACKUP_BUCKET' ..."
|
||||
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{
|
||||
\"spec\": {
|
||||
\"backup\": {
|
||||
\"barmanObjectStore\": {
|
||||
\"destinationPath\": \"s3://$GARAGE_BACKUP_BUCKET/\",
|
||||
\"endpointURL\": \"$GARAGE_S3_ENDPOINT\",
|
||||
\"s3Credentials\": {
|
||||
\"accessKeyId\": {\"name\": \"$GARAGE_BACKUP_SECRET_NAME\", \"key\": \"ACCESS_KEY_ID\"},
|
||||
\"secretAccessKey\": {\"name\": \"$GARAGE_BACKUP_SECRET_NAME\", \"key\": \"SECRET_ACCESS_KEY\"},
|
||||
\"region\": {\"name\": \"$GARAGE_BACKUP_SECRET_NAME\", \"key\": \"REGION\"}
|
||||
},
|
||||
\"wal\": {\"compression\": \"gzip\"},
|
||||
\"data\": {\"compression\": \"gzip\"}
|
||||
},
|
||||
\"retentionPolicy\": \"30d\"
|
||||
apply_barman_object_store() {
|
||||
if ! barman_crd_ready; then
|
||||
echo "ERROR: Barman Cloud plugin CRD not found (objectstores.barmancloud.cnpg.io). Install the plugin first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Applying Barman Cloud ObjectStore '$BARMAN_OBJECT_NAME' ..."
|
||||
kubectl apply -n "$NAMESPACE" -f - <<OBJECTSTORE
|
||||
apiVersion: barmancloud.cnpg.io/v1
|
||||
kind: ObjectStore
|
||||
metadata:
|
||||
name: $BARMAN_OBJECT_NAME
|
||||
spec:
|
||||
retentionPolicy: $RETENTION_POLICY
|
||||
configuration:
|
||||
destinationPath: s3://$GARAGE_BACKUP_BUCKET/
|
||||
endpointURL: $GARAGE_S3_ENDPOINT
|
||||
s3Credentials:
|
||||
accessKeyId:
|
||||
name: $GARAGE_BACKUP_SECRET_NAME
|
||||
key: ACCESS_KEY_ID
|
||||
secretAccessKey:
|
||||
name: $GARAGE_BACKUP_SECRET_NAME
|
||||
key: SECRET_ACCESS_KEY
|
||||
region:
|
||||
name: $GARAGE_BACKUP_SECRET_NAME
|
||||
key: REGION
|
||||
wal:
|
||||
compression: gzip
|
||||
data:
|
||||
compression: gzip
|
||||
OBJECTSTORE
|
||||
}
|
||||
|
||||
ensure_barman_plugin_config() {
|
||||
local plugin_names plugin_present
|
||||
plugin_names=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.spec.plugins[*].name}' 2>/dev/null || true)
|
||||
plugin_present=$(printf '%s\n' "$plugin_names" | tr ' ' '\n' | grep -F "$BARMAN_PLUGIN_NAME" || true)
|
||||
|
||||
if [[ -n "$plugin_present" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local plugins_json
|
||||
plugins_json=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o jsonpath='{.spec.plugins}' 2>/dev/null || true)
|
||||
|
||||
echo "Configuring CNPG to use Barman Cloud plugin '$BARMAN_PLUGIN_NAME' ..."
|
||||
if [[ -z "$plugins_json" || "$plugins_json" == "null" || "$plugins_json" == "[]" ]]; then
|
||||
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{
|
||||
\"spec\": {
|
||||
\"plugins\": [
|
||||
{
|
||||
\"name\": \"$BARMAN_PLUGIN_NAME\",
|
||||
\"isWALArchiver\": true,
|
||||
\"parameters\": {\"barmanObjectName\": \"$BARMAN_OBJECT_NAME\"}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}"
|
||||
}"
|
||||
else
|
||||
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type json -p "[
|
||||
{
|
||||
\"op\": \"add\",
|
||||
\"path\": \"/spec/plugins/-\",
|
||||
\"value\": {
|
||||
\"name\": \"$BARMAN_PLUGIN_NAME\",
|
||||
\"isWALArchiver\": true,
|
||||
\"parameters\": {\"barmanObjectName\": \"$BARMAN_OBJECT_NAME\"}
|
||||
}
|
||||
}
|
||||
]"
|
||||
fi
|
||||
}
|
||||
|
||||
remove_native_barman_config() {
|
||||
kubectl patch cluster "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" --type merge -p "{\"spec\":{\"backup\":null}}" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
trigger_backup() {
|
||||
@ -169,6 +240,11 @@ kind: Backup
|
||||
metadata:
|
||||
name: $backup_name
|
||||
spec:
|
||||
method: plugin
|
||||
pluginConfiguration:
|
||||
name: $BARMAN_PLUGIN_NAME
|
||||
parameters:
|
||||
barmanObjectName: $BARMAN_OBJECT_NAME
|
||||
cluster:
|
||||
name: $CNPG_CLUSTER_NAME
|
||||
BACKUP
|
||||
@ -186,7 +262,9 @@ case "$ACTION" in
|
||||
ensure_namespace
|
||||
ensure_cluster
|
||||
ensure_garage_bucket_and_key
|
||||
configure_cnpg_backup
|
||||
apply_barman_object_store
|
||||
ensure_barman_plugin_config
|
||||
remove_native_barman_config
|
||||
if [[ "$RUN_FIRST_BACKUP" == "1" ]]; then
|
||||
trigger_backup
|
||||
fi
|
||||
|
||||
@ -19,6 +19,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
# shellcheck disable=SC1090
|
||||
source "$SCRIPT_DIR/prole_cfg.sh"
|
||||
|
||||
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
|
||||
prole_set_mode "${2:-}"
|
||||
shift 2
|
||||
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
|
||||
prole_set_mode "${1#*=}"
|
||||
shift
|
||||
fi
|
||||
|
||||
ACTION=${1:-reset}
|
||||
|
||||
NAMESPACE=${NAMESPACE:-default}
|
||||
|
||||
@ -13,6 +13,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
# shellcheck disable=SC1090
|
||||
source "$SCRIPT_DIR/prole_cfg.sh"
|
||||
|
||||
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
|
||||
prole_set_mode "${2:-}"
|
||||
shift 2
|
||||
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
|
||||
prole_set_mode "${1#*=}"
|
||||
shift
|
||||
fi
|
||||
|
||||
ACTION=${1:-}
|
||||
VERSION=${2:-latest}
|
||||
|
||||
@ -34,6 +42,19 @@ ensure_tools() {
|
||||
done
|
||||
}
|
||||
|
||||
ensure_barman_plugin() {
|
||||
if kubectl get crd objectstores.barmancloud.cnpg.io >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
echo "Barman Cloud plugin CRD not found; installing plugin..."
|
||||
if [[ -x "$SCRIPT_DIR/init_cloudnative_pg.sh" ]]; then
|
||||
bash "$SCRIPT_DIR/init_cloudnative_pg.sh" install-barman-plugin
|
||||
else
|
||||
echo "ERROR: init_cloudnative_pg.sh not found; cannot install Barman Cloud plugin." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_namespace() {
|
||||
if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then
|
||||
echo "Creating namespace '$NAMESPACE' ..."
|
||||
@ -102,9 +123,11 @@ start() {
|
||||
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
|
||||
if [[ -z "${PROLE_MODE:-}" || "${PROLE_MODE}" == "k3d" ]]; then
|
||||
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
|
||||
fi
|
||||
|
||||
# 2. cnpg operator is loaded
|
||||
@ -112,6 +135,7 @@ start() {
|
||||
echo "ERROR: CloudNative-PG operator is not loaded." >&2
|
||||
exit 1
|
||||
fi
|
||||
ensure_barman_plugin
|
||||
|
||||
# 3. openbao is configured (local container)
|
||||
local openbao_url="${PROLE_OPENBAO_URL:-http://127.0.0.1:18200}"
|
||||
@ -204,6 +228,7 @@ restart() {
|
||||
deploy() {
|
||||
ensure_tools
|
||||
ensure_namespace
|
||||
ensure_barman_plugin
|
||||
local image
|
||||
if [[ "$VERSION" == "latest" ]]; then
|
||||
image=$(get_latest_image)
|
||||
|
||||
@ -16,6 +16,14 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
# shellcheck disable=SC1090
|
||||
source "$SCRIPT_DIR/prole_cfg.sh"
|
||||
|
||||
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
|
||||
prole_set_mode "${2:-}"
|
||||
shift 2
|
||||
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
|
||||
prole_set_mode "${1#*=}"
|
||||
shift
|
||||
fi
|
||||
|
||||
SUPABASE_NAMESPACE="${SUPABASE_NAMESPACE:-supabase}"
|
||||
PROLE_DB_SERVICE="${PROLE_DB_SERVICE:-prole-db-rw}"
|
||||
SUPABASE_DB_SERVICE="${SUPABASE_DB_SERVICE:-db}"
|
||||
@ -112,8 +120,10 @@ selector_pairs_to_yaml() {
|
||||
}
|
||||
|
||||
get_selector_pairs() {
|
||||
kubectl -n "$SUPABASE_NAMESPACE" get svc "$1" \
|
||||
-o jsonpath='{range $k,$v := .spec.selector}{$k}={$v},{end}' 2>/dev/null | sed 's/,$//'
|
||||
local out
|
||||
out=$(kubectl -n "$SUPABASE_NAMESPACE" get svc "$1" \
|
||||
-o jsonpath='{range $k,$v := .spec.selector}{$k}={$v},{end}' 2>/dev/null || true)
|
||||
printf '%s' "$out" | sed 's/,$//'
|
||||
}
|
||||
|
||||
restart_supabase() {
|
||||
@ -224,7 +234,8 @@ metadata:
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
${selector_yaml} ports:
|
||||
${selector_yaml}
|
||||
ports:
|
||||
- name: postgres
|
||||
port: ${SUPABASE_POSTGRES_PORT}
|
||||
targetPort: ${DB_PORT}
|
||||
|
||||
@ -84,6 +84,40 @@ _prole_cfg_extract_key() {
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
prole_normalize_mode() {
|
||||
local s
|
||||
s=$(printf '%s' "${1:-}" | tr 'A-Z' 'a-z')
|
||||
case "$s" in
|
||||
dev|k3d|k3d-*) echo "k3d"; return 0 ;;
|
||||
service|k3s|k3s-*) echo "k3s"; return 0 ;;
|
||||
prod|production|k8s|k8s-*) echo "k8s"; return 0 ;;
|
||||
prole-dev-cluster|k3d-prole-dev-cluster) echo "k3d"; return 0 ;;
|
||||
prole-service-cluster) echo "k3s"; return 0 ;;
|
||||
prole-prod-cluster) echo "k8s"; return 0 ;;
|
||||
esac
|
||||
echo "$s"
|
||||
}
|
||||
|
||||
prole_set_mode() {
|
||||
local raw="$1"
|
||||
if [[ -z "$raw" ]]; then
|
||||
echo "ERROR: --mode requires a value (k3d, k3s, k8s)" >&2
|
||||
exit 2
|
||||
fi
|
||||
local norm
|
||||
norm=$(prole_normalize_mode "$raw")
|
||||
case "$norm" in
|
||||
k3d|k3s|k8s)
|
||||
PROLE_MODE="$norm"
|
||||
export PROLE_MODE
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unsupported mode '$raw' (use k3d, k3s, or k8s)" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_prole_cfg_file=""
|
||||
if [[ -n "${PROLE_CONF:-}" && -f "$PROLE_CONF/prole.cfg" ]]; then
|
||||
_prole_cfg_file="$PROLE_CONF/prole.cfg"
|
||||
@ -127,6 +161,22 @@ if [[ -z "${NAMESPACE:-}" ]]; then
|
||||
fi
|
||||
export NAMESPACE=${NAMESPACE:-default}
|
||||
|
||||
if [[ -z "${PROLE_MODE:-}" ]]; then
|
||||
if [[ -n "${DEPLOYMENT_MODE:-}" ]]; then
|
||||
_prole_mode_guess=$(prole_normalize_mode "$DEPLOYMENT_MODE")
|
||||
case "$_prole_mode_guess" in
|
||||
k3d|k3s|k8s) PROLE_MODE="$_prole_mode_guess"; export PROLE_MODE ;;
|
||||
esac
|
||||
unset _prole_mode_guess
|
||||
elif [[ -n "${CLUSTER_ENV:-}" ]]; then
|
||||
_prole_mode_guess=$(prole_normalize_mode "$CLUSTER_ENV")
|
||||
case "$_prole_mode_guess" in
|
||||
k3d|k3s|k8s) PROLE_MODE="$_prole_mode_guess"; export PROLE_MODE ;;
|
||||
esac
|
||||
unset _prole_mode_guess
|
||||
fi
|
||||
fi
|
||||
|
||||
unset _prole_cfg_script_dir
|
||||
unset _prole_cfg_home_guess
|
||||
unset _prole_cfg_file
|
||||
|
||||
16
infrastructure/playbooks/k3s_start_single.yml
Normal file
16
infrastructure/playbooks/k3s_start_single.yml
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
- name: Start k3s on a single host
|
||||
hosts: k3s_hosts
|
||||
become: true
|
||||
serial: 1
|
||||
pre_tasks:
|
||||
- name: Require a single host target
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_play_hosts_all | length == 1
|
||||
fail_msg: "This playbook targets one host. Use --limit <host>."
|
||||
tasks:
|
||||
- name: Start k3s service
|
||||
ansible.builtin.import_role:
|
||||
name: k3s
|
||||
tasks_from: start
|
||||
33
infrastructure/playbooks/k3s_stop.yml
Normal file
33
infrastructure/playbooks/k3s_stop.yml
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
- name: Stop k3s agents first
|
||||
hosts: k3s_hosts
|
||||
become: true
|
||||
serial: 1
|
||||
tasks:
|
||||
- name: Stop k3s services on agent nodes
|
||||
ansible.builtin.import_role:
|
||||
name: k3s
|
||||
tasks_from: stop
|
||||
when: k3s_role != "server"
|
||||
|
||||
- name: Stop non-init k3s servers
|
||||
hosts: k3s_hosts
|
||||
become: true
|
||||
serial: 1
|
||||
tasks:
|
||||
- name: Stop k3s services on non-init servers
|
||||
ansible.builtin.import_role:
|
||||
name: k3s
|
||||
tasks_from: stop
|
||||
when: k3s_role == "server" and not (k3s_cluster_init | bool)
|
||||
|
||||
- name: Stop init k3s servers last
|
||||
hosts: k3s_hosts
|
||||
become: true
|
||||
serial: 1
|
||||
tasks:
|
||||
- name: Stop k3s services on init servers
|
||||
ansible.builtin.import_role:
|
||||
name: k3s
|
||||
tasks_from: stop
|
||||
when: k3s_role == "server" and (k3s_cluster_init | bool)
|
||||
28
infrastructure/roles/k3s/tasks/start.yml
Normal file
28
infrastructure/roles/k3s/tasks/start.yml
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
- name: Set k3s service name
|
||||
ansible.builtin.set_fact:
|
||||
k3s_service_name: "{{ 'k3s' if k3s_role == 'server' else 'k3s-agent' }}"
|
||||
|
||||
- name: Gather service facts
|
||||
ansible.builtin.service_facts:
|
||||
|
||||
- name: Ensure k3s service is installed
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (k3s_service_name + ".service") in ansible_facts.services
|
||||
fail_msg: "k3s service {{ k3s_service_name }} is not installed. Run the k3s role or reset playbook first."
|
||||
|
||||
- name: Start k3s service
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ k3s_service_name }}"
|
||||
state: started
|
||||
enabled: true
|
||||
no_block: true
|
||||
|
||||
- name: Wait for k3s to become active
|
||||
ansible.builtin.command: "systemctl is-active {{ k3s_service_name }}"
|
||||
register: k3s_service_state
|
||||
changed_when: false
|
||||
retries: "{{ [1, (k3s_start_timeout_seconds | default(120) | int) // 10] | max }}"
|
||||
delay: 10
|
||||
until: k3s_service_state.stdout == "active"
|
||||
13
infrastructure/roles/k3s/tasks/stop.yml
Normal file
13
infrastructure/roles/k3s/tasks/stop.yml
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
- name: Gather service facts
|
||||
ansible.builtin.service_facts:
|
||||
|
||||
- name: Stop k3s services if present
|
||||
ansible.builtin.service:
|
||||
name: "{{ item }}"
|
||||
state: stopped
|
||||
enabled: false
|
||||
loop:
|
||||
- k3s
|
||||
- k3s-agent
|
||||
when: (item + ".service") in ansible_facts.services
|
||||
75
install.py
75
install.py
@ -474,6 +474,32 @@ def _extract_yaml_scalar_from_text(text: str, key: str) -> str:
|
||||
return ''
|
||||
|
||||
|
||||
def _extract_inline_vault_block(text: str, key: str) -> str:
|
||||
lines = text.splitlines()
|
||||
for idx, raw in enumerate(lines):
|
||||
stripped = raw.strip()
|
||||
if not stripped or stripped.startswith('#'):
|
||||
continue
|
||||
if stripped.startswith(f"{key}:") and "!vault" in stripped:
|
||||
base_indent = len(raw) - len(raw.lstrip())
|
||||
block = []
|
||||
i = idx + 1
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
if not line.strip():
|
||||
i += 1
|
||||
continue
|
||||
indent = len(line) - len(line.lstrip())
|
||||
if indent <= base_indent:
|
||||
break
|
||||
block.append(line.strip())
|
||||
i += 1
|
||||
if block and block[0].startswith("$ANSIBLE_VAULT"):
|
||||
return "\n".join(block) + "\n"
|
||||
return ""
|
||||
return ""
|
||||
|
||||
|
||||
def _try_read_ansible_vault_value(vault_path: Path, key: str) -> str:
|
||||
if not vault_path.exists():
|
||||
return ''
|
||||
@ -499,9 +525,8 @@ def _try_read_ansible_vault_value(vault_path: Path, key: str) -> str:
|
||||
return ''
|
||||
|
||||
tmp_path = None
|
||||
args = ['ansible-vault', 'view', str(vault_path)]
|
||||
if password_file:
|
||||
args += ['--vault-password-file', password_file]
|
||||
password_file = password_file
|
||||
elif password:
|
||||
try:
|
||||
tmp = tempfile.NamedTemporaryFile(delete=False)
|
||||
@ -509,14 +534,14 @@ def _try_read_ansible_vault_value(vault_path: Path, key: str) -> str:
|
||||
tmp.flush()
|
||||
tmp.close()
|
||||
tmp_path = tmp.name
|
||||
args += ['--vault-password-file', tmp_path]
|
||||
password_file = tmp_path
|
||||
except Exception:
|
||||
tmp_path = None
|
||||
return ''
|
||||
|
||||
try:
|
||||
def _vault_view(path: str) -> str:
|
||||
res = subprocess.run(
|
||||
args,
|
||||
['ansible-vault', 'view', path] + (['--vault-password-file', password_file] if password_file else []),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
@ -525,7 +550,31 @@ def _try_read_ansible_vault_value(vault_path: Path, key: str) -> str:
|
||||
)
|
||||
if res.returncode != 0:
|
||||
return ''
|
||||
return _extract_yaml_scalar_from_text(res.stdout or '', key)
|
||||
return res.stdout or ''
|
||||
|
||||
try:
|
||||
output = _vault_view(str(vault_path))
|
||||
if output:
|
||||
return _extract_yaml_scalar_from_text(output, key)
|
||||
|
||||
# Inline vault: extract the block and decrypt separately.
|
||||
try:
|
||||
raw_text = vault_path.read_text()
|
||||
except Exception:
|
||||
raw_text = ''
|
||||
inline_block = _extract_inline_vault_block(raw_text, key)
|
||||
if not inline_block:
|
||||
return ''
|
||||
tmp_inline = tempfile.NamedTemporaryFile(delete=False)
|
||||
tmp_inline.write(inline_block.encode('utf-8'))
|
||||
tmp_inline.flush()
|
||||
tmp_inline.close()
|
||||
output = _vault_view(tmp_inline.name)
|
||||
try:
|
||||
os.unlink(tmp_inline.name)
|
||||
except Exception:
|
||||
pass
|
||||
return output.strip()
|
||||
except Exception:
|
||||
return ''
|
||||
finally:
|
||||
@ -2975,7 +3024,8 @@ class ProleInstaller:
|
||||
'DEPLOYMENT_MODE': mode,
|
||||
'DEPLOYMENT_TARGET': target_label,
|
||||
'NAMESPACE': (self.db_namespace.get() or '').strip(),
|
||||
'DB_HOST_PORT': (self.db_host_port.get() or '5432').strip()
|
||||
'DB_HOST_PORT': (self.db_host_port.get() or '5432').strip(),
|
||||
'PROLE_OPENTOFU_URL': _default_opentofu_pipeline_url(),
|
||||
}
|
||||
# Add any other values already in self.prole_cfg_data['Global']
|
||||
globals_to_save.update(self.prole_cfg_data.get('Global', {}))
|
||||
@ -9589,6 +9639,9 @@ class ProleSilentInstaller:
|
||||
def _get_input_bool(self, key: str, default: bool = False) -> bool:
|
||||
return _parse_bool(self._get_input(key, None), default=default)
|
||||
|
||||
def _deployment_mode(self) -> str:
|
||||
return _deployment_mode_from_env(self._get_input('init_cluster.cluster_env', ''))
|
||||
|
||||
def _secret_namespace(self) -> str:
|
||||
ns = (self._get_input('init_password.db_namespace', '') or '').strip()
|
||||
if not ns:
|
||||
@ -10168,7 +10221,8 @@ class ProleSilentInstaller:
|
||||
'DB_HOST_PORT': (self._get_input('init_password.db_host_port', '5432') or '5432').strip(),
|
||||
'DOCKER_IMPORT_DIR': self.docker_import_dir or '',
|
||||
'PROLE_K3S_SERVER': (self._get_input('init_cluster.k3s_server_url', '') or '').strip(),
|
||||
'PROLE_K3S_TOKEN': _encrypt_cfg_secret(self._get_input('init_cluster.k3s_token', '') or '')
|
||||
'PROLE_K3S_TOKEN': _encrypt_cfg_secret(self._get_input('init_cluster.k3s_token', '') or ''),
|
||||
'PROLE_OPENTOFU_URL': _default_opentofu_pipeline_url(),
|
||||
}
|
||||
globals_to_save.update(self.prole_cfg_data.get('Global', {}))
|
||||
globals_to_save['DB_PASSWORD'] = self._secret_cfg_value('Global', 'DB_PASSWORD', db_pw, 'db', 'password')
|
||||
@ -10327,11 +10381,14 @@ class ProleSilentInstaller:
|
||||
'PROLE_DATA': self._get_input('env_setup.PROLE_DATA', ''),
|
||||
'PROLE_LOGS': self._get_input('env_setup.PROLE_LOGS', ''),
|
||||
'PROLE_SERVICE': self._get_input('env_setup.PROLE_SERVICE', ''),
|
||||
'PROLE_OPENTOFU_URL': (os.environ.get('PROLE_OPENTOFU_URL') or '').strip(),
|
||||
}
|
||||
defaults = self._env_defaults(self._get_input('env_setup.NAMESPACE', ''))
|
||||
for k in vals:
|
||||
if not vals[k]:
|
||||
vals[k] = defaults.get(k, '')
|
||||
if not vals.get('PROLE_OPENTOFU_URL'):
|
||||
vals['PROLE_OPENTOFU_URL'] = _default_opentofu_pipeline_url()
|
||||
vals['NAMESPACE'] = self._get_input('env_setup.NAMESPACE', '')
|
||||
|
||||
if not vals.get('PROLE_HOME'):
|
||||
@ -10343,7 +10400,7 @@ class ProleSilentInstaller:
|
||||
self._save_env_to_file(vals)
|
||||
self.reload_env_from_shell()
|
||||
|
||||
for k in ('PROLE_HOME', 'PROLE_CONF', 'PROLE_DATA', 'PROLE_LOGS', 'PROLE_SERVICE'):
|
||||
for k in ('PROLE_HOME', 'PROLE_CONF', 'PROLE_DATA', 'PROLE_LOGS', 'PROLE_SERVICE', 'PROLE_OPENTOFU_URL'):
|
||||
self.prole_cfg_data['System Environment'][k] = vals[k]
|
||||
|
||||
def _step_init_password(self) -> None:
|
||||
|
||||
@ -115,6 +115,8 @@ def get_docker_build_platform_args(target_env: Optional[str] = None) -> list[str
|
||||
if override:
|
||||
return ["--platform", override]
|
||||
env_key = (target_env or "").strip().lower()
|
||||
if env_key == "dev" and is_apple_silicon():
|
||||
return ["--platform", "linux/arm64"]
|
||||
if env_key in ("service", "k3s", "prole-service-cluster"):
|
||||
return ["--platform", "linux/arm64"]
|
||||
if is_apple_silicon():
|
||||
|
||||
@ -13,6 +13,7 @@ import os
|
||||
import subprocess
|
||||
import getpass
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable
|
||||
|
||||
@ -48,7 +49,7 @@ class ProleNcursesInstaller:
|
||||
self.selected_env_index = 0
|
||||
self.kubectx_list = self._get_kubectx_list()
|
||||
self.selected_kubectx_index = 0
|
||||
self.k3s_services_status = {"registry": "Unknown", "openbao": "Unknown"}
|
||||
self.k3s_services_status = {"registry": "Unknown", "openbao": "Unknown", "opentofu": "Unknown"}
|
||||
self.prod_artifacts_path = InputField(self.main_content_win, 15, 4, 50)
|
||||
self.prod_artifacts_path.set_value(str(self.project_root / "data" / "staging"))
|
||||
|
||||
@ -318,13 +319,42 @@ class ProleNcursesInstaller:
|
||||
pass
|
||||
return ["default"]
|
||||
|
||||
def _write_k3s_kubeconfig(self, server: str, token: str) -> Path:
|
||||
if not server.startswith("http"):
|
||||
server = f"https://{server}"
|
||||
cfg = (
|
||||
"apiVersion: v1\n"
|
||||
"kind: Config\n"
|
||||
"clusters:\n"
|
||||
"- cluster:\n"
|
||||
f" server: {server}\n"
|
||||
" insecure-skip-tls-verify: true\n"
|
||||
" name: prole-k3s\n"
|
||||
"contexts:\n"
|
||||
"- context:\n"
|
||||
" cluster: prole-k3s\n"
|
||||
" user: prole-k3s\n"
|
||||
" name: prole-k3s\n"
|
||||
"current-context: prole-k3s\n"
|
||||
"users:\n"
|
||||
"- name: prole-k3s\n"
|
||||
" user:\n"
|
||||
f" token: {token}\n"
|
||||
)
|
||||
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".kubeconfig")
|
||||
tmp.write(cfg.encode("utf-8"))
|
||||
tmp.flush()
|
||||
tmp.close()
|
||||
os.chmod(tmp.name, 0o600)
|
||||
return Path(tmp.name)
|
||||
|
||||
def _verify_k3s_services(self):
|
||||
"""Verify registry:2 and openbao on remote k3s cluster."""
|
||||
"""Verify registry:2, OpenBao, and OpenTofu on remote k3s cluster."""
|
||||
def _verify():
|
||||
self.k3s_services_status = {"registry": "Checking...", "openbao": "Checking..."}
|
||||
self.k3s_services_status = {"registry": "Checking...", "openbao": "Checking...", "opentofu": "Checking..."}
|
||||
|
||||
if not self.k3s_token:
|
||||
self.k3s_services_status = {"registry": "Missing token", "openbao": "Missing token"}
|
||||
self.k3s_services_status = {"registry": "Missing token", "openbao": "Missing token", "opentofu": "Missing token"}
|
||||
return
|
||||
|
||||
base_cmd = [
|
||||
@ -348,23 +378,47 @@ class ProleNcursesInstaller:
|
||||
self.k3s_services_status["openbao"] = "Good"
|
||||
else:
|
||||
self.k3s_services_status["openbao"] = "Failing"
|
||||
|
||||
# Check opentofu
|
||||
if "opentofu" in res.stdout.lower():
|
||||
self.k3s_services_status["opentofu"] = "Good"
|
||||
else:
|
||||
self.k3s_services_status["opentofu"] = "Failing"
|
||||
except Exception as e:
|
||||
self.status_message = f"K3s connection error: {str(e)}"
|
||||
self.k3s_services_status = {"registry": "Error", "openbao": "Error"}
|
||||
self.k3s_services_status = {"registry": "Error", "openbao": "Error", "opentofu": "Error"}
|
||||
|
||||
threading.Thread(target=_verify).start()
|
||||
|
||||
def _deploy_k3s_services(self):
|
||||
"""Deploy registry and openbao to remote k3s cluster."""
|
||||
"""Deploy registry, OpenBao, and OpenTofu to remote k3s cluster."""
|
||||
def _deploy():
|
||||
self.status_message = "Deploying services to remote k3s..."
|
||||
# In a real scenario, we'd run a script or apply manifests
|
||||
# base_cmd = [...]
|
||||
# subprocess.run(base_cmd + ["apply", "-f", ...])
|
||||
|
||||
# Simulate deployment delay
|
||||
import time
|
||||
time.sleep(2)
|
||||
if not self.k3s_server or not self.k3s_token:
|
||||
self.status_message = "Missing k3s server or token."
|
||||
return
|
||||
|
||||
env = os.environ.copy()
|
||||
env["PROLE_HOME"] = str(self.project_root)
|
||||
env["PROLE_SERVICE"] = str(self.project_root)
|
||||
env["NAMESPACE"] = self.db_namespace or "default"
|
||||
env["DB_PASSWORD"] = self.db_password or ""
|
||||
env["OPENTOFU_ADMIN_PASSWORD"] = self.db_password or ""
|
||||
env["PROLE_MODE"] = "k3s"
|
||||
|
||||
kubeconfig_path = None
|
||||
try:
|
||||
kubeconfig_path = self._write_k3s_kubeconfig(self.k3s_server, self.k3s_token)
|
||||
env["KUBECONFIG"] = str(kubeconfig_path)
|
||||
self.controller.run_script("init_openbao.sh", args=["-n", env["NAMESPACE"], "update"], env=env)
|
||||
self.controller.run_script("init_opentofu.sh", args=["-n", env["NAMESPACE"], "update"], env=env)
|
||||
finally:
|
||||
if kubeconfig_path:
|
||||
try:
|
||||
os.unlink(kubeconfig_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self._verify_k3s_services()
|
||||
self.status_message = "Deployment triggered on remote k3s."
|
||||
|
||||
|
||||
@ -12,8 +12,8 @@ data:
|
||||
|
||||
[realms]
|
||||
PROLE.ORG = {
|
||||
kdc = 10.0.0.3
|
||||
admin_server = 10.0.0.3
|
||||
kdc = kdc.prole.org
|
||||
admin_server = kdc.prole.org
|
||||
}
|
||||
|
||||
[domain_realm]
|
||||
|
||||
111
k8s/opentofu/deployment.yaml
Normal file
111
k8s/opentofu/deployment.yaml
Normal file
@ -0,0 +1,111 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: opentofu-nginx
|
||||
data:
|
||||
default.conf: |
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
|
||||
auth_basic "OpenTofu";
|
||||
auth_basic_user_file /etc/nginx/auth/auth;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
}
|
||||
}
|
||||
index.html: |
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>OpenTofu</title>
|
||||
<style>
|
||||
body { font-family: "SF Pro Text", "Segoe UI", sans-serif; background: #f5f5f0; color: #1d1d1f; }
|
||||
.wrap { max-width: 720px; margin: 60px auto; padding: 32px; background: #fff; border-radius: 16px; box-shadow: 0 8px 24px rgba(0,0,0,0.08); }
|
||||
h1 { margin-top: 0; font-size: 28px; }
|
||||
code { background: #f2f2f2; padding: 2px 6px; border-radius: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>OpenTofu Pipeline Ready</h1>
|
||||
<p>This service hosts the OpenTofu control plane for Prole deployments.</p>
|
||||
<p>Pipeline root (on disk): <code>deploy/opentofu/k3s</code></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: opentofu
|
||||
labels:
|
||||
app: opentofu
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: opentofu
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: opentofu
|
||||
spec:
|
||||
containers:
|
||||
- name: opentofu-ui
|
||||
image: nginx:1.27-alpine
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
volumeMounts:
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/conf.d
|
||||
- name: ui-content
|
||||
mountPath: /usr/share/nginx/html
|
||||
- name: opentofu-auth
|
||||
mountPath: /etc/nginx/auth
|
||||
readOnly: true
|
||||
- name: opentofu-runner
|
||||
image: ghcr.io/opentofu/opentofu:latest
|
||||
command: ["sh", "-c", "tofu version && tail -f /dev/null"]
|
||||
env:
|
||||
- name: OPENTOFU_ADMIN_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: opentofu-admin
|
||||
key: admin_password
|
||||
volumes:
|
||||
- name: nginx-config
|
||||
configMap:
|
||||
name: opentofu-nginx
|
||||
items:
|
||||
- key: default.conf
|
||||
path: default.conf
|
||||
- name: ui-content
|
||||
configMap:
|
||||
name: opentofu-nginx
|
||||
items:
|
||||
- key: index.html
|
||||
path: index.html
|
||||
- name: opentofu-auth
|
||||
secret:
|
||||
secretName: opentofu-admin
|
||||
items:
|
||||
- key: auth
|
||||
path: auth
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: opentofu
|
||||
spec:
|
||||
selector:
|
||||
app: opentofu
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: http
|
||||
@ -5,6 +5,7 @@ resources:
|
||||
- garage-configmap.yaml
|
||||
- garage-statefulset.yaml
|
||||
- garage-service.yaml
|
||||
- prole-db-barman-objectstore.yaml
|
||||
- prole-db.yaml
|
||||
- prole-db-postgres-service.yaml
|
||||
- prole-configmap.yaml
|
||||
|
||||
23
k8s/prole/prole-db-barman-objectstore.yaml
Normal file
23
k8s/prole/prole-db-barman-objectstore.yaml
Normal file
@ -0,0 +1,23 @@
|
||||
apiVersion: barmancloud.cnpg.io/v1
|
||||
kind: ObjectStore
|
||||
metadata:
|
||||
name: prole-db-barman-objectstore
|
||||
spec:
|
||||
retentionPolicy: 30d
|
||||
configuration:
|
||||
destinationPath: s3://prole-db-backups/
|
||||
endpointURL: http://garage:3900
|
||||
s3Credentials:
|
||||
accessKeyId:
|
||||
name: prole-db-barman-s3
|
||||
key: ACCESS_KEY_ID
|
||||
secretAccessKey:
|
||||
name: prole-db-barman-s3
|
||||
key: SECRET_ACCESS_KEY
|
||||
region:
|
||||
name: prole-db-barman-s3
|
||||
key: REGION
|
||||
wal:
|
||||
compression: gzip
|
||||
data:
|
||||
compression: gzip
|
||||
@ -47,8 +47,14 @@ spec:
|
||||
walStorage:
|
||||
size: 1Gi
|
||||
|
||||
plugins:
|
||||
- name: barman-cloud.cloudnative-pg.io
|
||||
isWALArchiver: true
|
||||
parameters:
|
||||
barmanObjectName: prole-db-barman-objectstore
|
||||
|
||||
monitoring:
|
||||
enablePodMonitor: true
|
||||
enablePodMonitor: false
|
||||
|
||||
# managed rw service
|
||||
managed:
|
||||
|
||||
@ -66,29 +66,14 @@ spec:
|
||||
# volumeMode: Filesystem
|
||||
walStorage:
|
||||
size: 1Gi
|
||||
|
||||
backup:
|
||||
barmanObjectStore:
|
||||
destinationPath: s3://prole-db-backups/
|
||||
endpointURL: http://garage:3900
|
||||
s3Credentials:
|
||||
accessKeyId:
|
||||
name: prole-db-barman-s3
|
||||
key: ACCESS_KEY_ID
|
||||
secretAccessKey:
|
||||
name: prole-db-barman-s3
|
||||
key: SECRET_ACCESS_KEY
|
||||
region:
|
||||
name: prole-db-barman-s3
|
||||
key: REGION
|
||||
wal:
|
||||
compression: gzip
|
||||
data:
|
||||
compression: gzip
|
||||
retentionPolicy: 30d
|
||||
plugins:
|
||||
- name: barman-cloud.cloudnative-pg.io
|
||||
isWALArchiver: true
|
||||
parameters:
|
||||
barmanObjectName: prole-db-barman-objectstore
|
||||
|
||||
monitoring:
|
||||
enablePodMonitor: true
|
||||
enablePodMonitor: false
|
||||
|
||||
# managed rw service
|
||||
managed:
|
||||
|
||||
@ -128,30 +128,23 @@ RUN apt-get remove -y --purge --autoremove build-essential python3-dev && rm -rf
|
||||
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
COPY prole-db-entrypoint.sh /usr/local/bin/prole-entrypoint.sh
|
||||
COPY 10_pg_tde_openbao.sh /docker-entrypoint-initdb.d/10_pg_tde_openbao.sh
|
||||
COPY prole-db-ssh-openbao.sh /usr/local/bin/prole-db-ssh-openbao.sh
|
||||
|
||||
# staged copies of the pgdata/*.conf files
|
||||
COPY postgresql/*.conf /etc/postgresql/17/main/
|
||||
RUN chown postgres:postgres /etc/postgresql/17/main/*.conf
|
||||
|
||||
# 7) Create new user and setup SSH authorized keys
|
||||
# 7) Create new user
|
||||
ARG PROLE_USER=prole
|
||||
ARG PROLE_SSH_PUB_KEY=""
|
||||
RUN set -eux; \
|
||||
if [ -n "$PROLE_USER" ] && [ "$PROLE_USER" != "root" ] && [ "$PROLE_USER" != "postgres" ]; then \
|
||||
if ! id -u "$PROLE_USER" >/dev/null 2>&1; then \
|
||||
useradd -m -s /bin/bash "$PROLE_USER"; \
|
||||
fi; \
|
||||
if [ -n "$PROLE_SSH_PUB_KEY" ]; then \
|
||||
mkdir -p "/home/$PROLE_USER/.ssh"; \
|
||||
echo "$PROLE_SSH_PUB_KEY" >> "/home/$PROLE_USER/.ssh/authorized_keys"; \
|
||||
chown -R "$PROLE_USER:$PROLE_USER" "/home/$PROLE_USER/.ssh"; \
|
||||
chmod 700 "/home/$PROLE_USER/.ssh"; \
|
||||
chmod 600 "/home/$PROLE_USER/.ssh/authorized_keys"; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
RUN set -eux; \
|
||||
chmod +x /usr/local/bin/docker-entrypoint.sh /usr/local/bin/prole-entrypoint.sh /docker-entrypoint-initdb.d/10_pg_tde_openbao.sh
|
||||
chmod +x /usr/local/bin/docker-entrypoint.sh /usr/local/bin/prole-entrypoint.sh /usr/local/bin/prole-db-ssh-openbao.sh /docker-entrypoint-initdb.d/10_pg_tde_openbao.sh
|
||||
|
||||
EXPOSE 5432
|
||||
CMD ["postgres"]
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
if [[ -x /usr/local/bin/prole-db-ssh-openbao.sh ]]; then
|
||||
/usr/local/bin/prole-db-ssh-openbao.sh || true
|
||||
fi
|
||||
|
||||
# If user runs the default "postgres" command, optionally force pg_tde preload.
|
||||
if [[ "${1:-}" == "postgres" ]] && [[ "${ENABLE_PG_TDE:-}" == "1" ]]; then
|
||||
# Only inject if user didn't already set shared_preload_libraries explicitly
|
||||
@ -10,4 +14,4 @@ if [[ "${1:-}" == "postgres" ]] && [[ "${ENABLE_PG_TDE:-}" == "1" ]]; then
|
||||
fi
|
||||
|
||||
# Delegate everything else to the official entrypoint
|
||||
exec /usr/local/bin/docker-entrypoint.sh "$@"
|
||||
exec /usr/local/bin/docker-entrypoint.sh "$@"
|
||||
|
||||
91
prole-db/prole-db-ssh-openbao.sh
Normal file
91
prole-db/prole-db-ssh-openbao.sh
Normal file
@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
PROLE_USER="${PROLE_USER:-prole}"
|
||||
TOKEN="${OPENBAO_TOKEN:-}"
|
||||
TOKEN_FILE="${OPENBAO_TOKEN_FILE:-/run/secrets/openbao_token}"
|
||||
|
||||
if [[ -z "$TOKEN" && -f "$TOKEN_FILE" ]]; then
|
||||
TOKEN=$(cat "$TOKEN_FILE")
|
||||
fi
|
||||
|
||||
if [[ -z "$TOKEN" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
NAMESPACE="${NAMESPACE:-}"
|
||||
if [[ -z "$NAMESPACE" && -f /var/run/secrets/kubernetes.io/serviceaccount/namespace ]]; then
|
||||
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
|
||||
fi
|
||||
|
||||
OPENBAO_ADDR="${OPENBAO_ADDR:-${PROLE_OPENBAO_URL:-}}"
|
||||
if [[ -z "$OPENBAO_ADDR" && -n "$NAMESPACE" ]]; then
|
||||
OPENBAO_ADDR="http://openbao.${NAMESPACE}.svc.cluster.local:8200"
|
||||
fi
|
||||
|
||||
if [[ -z "$OPENBAO_ADDR" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
OPENBAO_SSH_MOUNT="${OPENBAO_SSH_MOUNT:-kv}"
|
||||
OPENBAO_SSH_PATH="${OPENBAO_SSH_PATH:-prole/${NAMESPACE}/admin}"
|
||||
|
||||
if [[ -z "$OPENBAO_SSH_PATH" || "$OPENBAO_SSH_PATH" == "prole//admin" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
response=$(curl -fsS -H "X-Vault-Token: $TOKEN" \
|
||||
"$OPENBAO_ADDR/v1/${OPENBAO_SSH_MOUNT}/data/${OPENBAO_SSH_PATH}" 2>/dev/null || true)
|
||||
|
||||
if [[ -z "$response" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ssh_key=$(python3 - <<'PY' <<<"$response"
|
||||
import base64
|
||||
import json
|
||||
import sys
|
||||
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
|
||||
data = payload.get("data", {}).get("data", {})
|
||||
key_b64 = data.get("admin_public_key_b64")
|
||||
if key_b64:
|
||||
try:
|
||||
print(base64.b64decode(key_b64).decode().strip())
|
||||
sys.exit(0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for k in ("admin_public_key", "ssh_public_key", "public_key"):
|
||||
val = data.get(k)
|
||||
if val:
|
||||
print(str(val).strip())
|
||||
break
|
||||
PY
|
||||
)
|
||||
|
||||
if [[ -z "$ssh_key" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! id -u "$PROLE_USER" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ssh_dir="/home/$PROLE_USER/.ssh"
|
||||
auth_keys="${ssh_dir}/authorized_keys"
|
||||
|
||||
mkdir -p "$ssh_dir"
|
||||
touch "$auth_keys"
|
||||
|
||||
if ! grep -qxF "$ssh_key" "$auth_keys"; then
|
||||
printf '%s\n' "$ssh_key" >> "$auth_keys"
|
||||
fi
|
||||
|
||||
chown -R "$PROLE_USER:$PROLE_USER" "$ssh_dir"
|
||||
chmod 700 "$ssh_dir"
|
||||
chmod 600 "$auth_keys"
|
||||
@ -10,7 +10,7 @@ If you wish to deploy Supabase, you can use the `./supabase/deploy.sh` script wh
|
||||
|
||||
## Postgres connectivity
|
||||
|
||||
Supabase connects to the CNPG primary via the service `prole-db-postgres` on port 5432. The service selector targets the CNPG primary pod:
|
||||
Supabase connects to the CNPG primary via the service `prole-db-rw` on port 5432. This service is managed by CNPG and always routes to the primary pod (selectors are CNPG-managed, e.g.):
|
||||
|
||||
- `cnpg.io/cluster: prole-db`
|
||||
- `role: primary`
|
||||
@ -51,7 +51,7 @@ kubectl apply -k k8s/prole
|
||||
```bash
|
||||
kubectl get deploy supabase
|
||||
kubectl get svc supabase
|
||||
kubectl get svc prole-db-postgres
|
||||
kubectl get svc prole-db-rw
|
||||
```
|
||||
|
||||
5) Check Supabase logs if needed:
|
||||
@ -68,7 +68,7 @@ If you need Supabase to connect to a different CNPG cluster name or namespace, u
|
||||
|
||||
- If Supabase cannot connect to Postgres, confirm:
|
||||
- The `prole-db` cluster is healthy and a primary is elected.
|
||||
- The `prole-db-postgres` service resolves to the primary pod.
|
||||
- The `prole-db-rw` service resolves to the primary pod.
|
||||
- The `prole-db-user` secret exists and contains valid credentials.
|
||||
|
||||
- If HTTPS does not respond:
|
||||
@ -77,7 +77,7 @@ If you need Supabase to connect to a different CNPG cluster name or namespace, u
|
||||
|
||||
## Files referenced
|
||||
|
||||
- `prole-db-postgres-service.yaml`
|
||||
- `prole-db-postgres-service.yaml` (optional legacy service; Supabase uses `prole-db-rw`)
|
||||
- `k8s/prole/kustomization.yaml`
|
||||
- `install.py` (optional feature toggle persistence)
|
||||
- `./supabase/deploy.sh`
|
||||
|
||||
59
prole/ansible/check_k3s_endpoint.yml
Normal file
59
prole/ansible/check_k3s_endpoint.yml
Normal file
@ -0,0 +1,59 @@
|
||||
---
|
||||
- name: Validate k3s API endpoint reachability
|
||||
hosts: k3s_hosts
|
||||
gather_facts: false
|
||||
become: false
|
||||
vars:
|
||||
k3s_default_port: 6443
|
||||
tasks:
|
||||
- name: Select k3s init host
|
||||
ansible.builtin.set_fact:
|
||||
k3s_init_host: "{{ item }}"
|
||||
when: hostvars[item].k3s_cluster_init | default(false) | bool
|
||||
loop: "{{ groups['k3s_hosts'] }}"
|
||||
run_once: true
|
||||
|
||||
- name: Select k3s server URL (if provided)
|
||||
ansible.builtin.set_fact:
|
||||
k3s_api_url: "{{ hostvars[item].k3s_server_url }}"
|
||||
when:
|
||||
- hostvars[item].k3s_server_url is defined
|
||||
- hostvars[item].k3s_server_url | length > 0
|
||||
loop: "{{ groups['k3s_hosts'] }}"
|
||||
run_once: true
|
||||
|
||||
- name: Fallback to init host URL
|
||||
ansible.builtin.set_fact:
|
||||
k3s_api_url: "https://{{ k3s_init_host | default(groups['k3s_hosts'][0]) }}:{{ k3s_default_port }}"
|
||||
when: k3s_api_url is not defined or k3s_api_url | length == 0
|
||||
run_once: true
|
||||
|
||||
- name: Extract k3s API host
|
||||
ansible.builtin.set_fact:
|
||||
k3s_api_host: "{{ k3s_api_url | regex_replace('^https?://', '') | regex_replace(':.*$', '') }}"
|
||||
run_once: true
|
||||
|
||||
- name: Check TCP port 6443
|
||||
ansible.builtin.wait_for:
|
||||
host: "{{ k3s_api_host }}"
|
||||
port: "{{ k3s_default_port }}"
|
||||
timeout: 10
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
|
||||
- name: Check /readyz endpoint
|
||||
ansible.builtin.uri:
|
||||
url: "{{ k3s_api_url }}/readyz"
|
||||
method: GET
|
||||
status_code: [200, 401, 403]
|
||||
validate_certs: false
|
||||
timeout: 5
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
|
||||
- name: Report k3s API endpoint
|
||||
ansible.builtin.debug:
|
||||
msg: "k3s API reachable at {{ k3s_api_url }}"
|
||||
run_once: true
|
||||
@ -7,26 +7,12 @@ if [[ -f "$PROJECT_ROOT/env.sh" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$PROJECT_ROOT/env.sh"
|
||||
fi
|
||||
DEV_HOME="${DEV_HOME:-$HOME/dev}"
|
||||
DEV_HOME="${DEV_HOME/#\~/$HOME}"
|
||||
SUPABASE_DIR="$DEV_HOME/supabase"
|
||||
DOCKER_DIR="$SUPABASE_DIR/docker"
|
||||
COMPOSE_FILE="$DOCKER_DIR/docker-compose.yml"
|
||||
DEV_COMPOSE_FILE="$DOCKER_DIR/dev/docker-compose.dev.yml"
|
||||
ENV_EXAMPLE="$DOCKER_DIR/.env.example"
|
||||
ENV_FILE="$DOCKER_DIR/.env"
|
||||
SUPABASE_K8S_DIR="${SUPABASE_K8S_DIR:-$SCRIPT_DIR/../build/supabase-k8s}"
|
||||
DOCKER_IMPORT_DIR="${DOCKER_IMPORT_DIR:-$PROJECT_ROOT/data/docker-import}"
|
||||
SUPABASE_IMAGE_PLATFORM="${SUPABASE_IMAGE_PLATFORM:-}"
|
||||
SUPABASE_IMAGE_PLATFORMS="${SUPABASE_IMAGE_PLATFORMS:-linux/amd64 linux/arm64}"
|
||||
SUPABASE_POSTGRES_PORT="${SUPABASE_POSTGRES_PORT:-15432}"
|
||||
PROLE_DB_SERVICE="${PROLE_DB_SERVICE:-prole-db-postgres}"
|
||||
PROLE_DB_NAMESPACE="${PROLE_DB_NAMESPACE:-}"
|
||||
|
||||
MODE="k3d"
|
||||
USE_DEV_HELPERS="false"
|
||||
FOREGROUND="false"
|
||||
FORCE="true"
|
||||
PROLE_CFG_PATH=""
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
@ -40,11 +26,12 @@ Usage:
|
||||
./deploy.sh [options]
|
||||
|
||||
Options:
|
||||
--mode <local|k3d> Deployment mode ('local' for Docker Compose, 'k3d' for Kubernetes; default: k3d)
|
||||
--mode <local|k3d|k8s> Deployment mode ('local' for Docker Compose, 'k3d' for local k3d, 'k8s' for generic k8s/k3s; default: k3d)
|
||||
--with-dev-helpers Include docker/dev/docker-compose.dev.yml
|
||||
--foreground Run docker compose in the foreground (default: detached, local mode only)
|
||||
-f, --force Reset the Supabase namespace before applying manifests (k3d only; default)
|
||||
--no-force Skip namespace reset (k3d only)
|
||||
-c, --config <path> Path to prole.cfg (loads environment defaults)
|
||||
-h, --help Show help
|
||||
|
||||
Notes:
|
||||
@ -72,6 +59,49 @@ log() {
|
||||
echo "==> $*"
|
||||
}
|
||||
|
||||
apply_defaults() {
|
||||
DEV_HOME="${DEV_HOME:-$HOME/dev}"
|
||||
DEV_HOME="${DEV_HOME/#\~/$HOME}"
|
||||
SUPABASE_DIR="$DEV_HOME/supabase"
|
||||
DOCKER_DIR="$SUPABASE_DIR/docker"
|
||||
COMPOSE_FILE="$DOCKER_DIR/docker-compose.yml"
|
||||
DEV_COMPOSE_FILE="$DOCKER_DIR/dev/docker-compose.dev.yml"
|
||||
ENV_EXAMPLE="$DOCKER_DIR/.env.example"
|
||||
ENV_FILE="$DOCKER_DIR/.env"
|
||||
SUPABASE_K8S_DIR="${SUPABASE_K8S_DIR:-$SCRIPT_DIR/../build/supabase-k8s}"
|
||||
DOCKER_IMPORT_DIR="${DOCKER_IMPORT_DIR:-$PROJECT_ROOT/data/docker-import}"
|
||||
SUPABASE_IMAGE_PLATFORM="${SUPABASE_IMAGE_PLATFORM:-}"
|
||||
SUPABASE_IMAGE_PLATFORMS="${SUPABASE_IMAGE_PLATFORMS:-linux/amd64 linux/arm64}"
|
||||
SUPABASE_POSTGRES_PORT="${SUPABASE_POSTGRES_PORT:-15432}"
|
||||
PROLE_DB_SERVICE="${PROLE_DB_SERVICE:-prole-db-rw}"
|
||||
PROLE_DB_NAMESPACE="${PROLE_DB_NAMESPACE:-}"
|
||||
}
|
||||
|
||||
load_prole_cfg() {
|
||||
local cfg_loader="$PROJECT_ROOT/etc/prole_cfg.sh"
|
||||
local cfg_path="${PROLE_CFG_PATH:-}"
|
||||
|
||||
if [[ -z "$cfg_path" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n "$cfg_path" ]]; then
|
||||
if [[ -d "$cfg_path" ]]; then
|
||||
cfg_path="$cfg_path/prole.cfg"
|
||||
fi
|
||||
[[ -f "$cfg_path" ]] || die "Config file not found: $cfg_path"
|
||||
PROLE_CONF="$(cd "$(dirname "$cfg_path")" && pwd)"
|
||||
export PROLE_CONF
|
||||
fi
|
||||
|
||||
if [[ -f "$cfg_loader" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$cfg_loader"
|
||||
else
|
||||
warn "prole_cfg.sh not found; config defaults may be incomplete."
|
||||
fi
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
|
||||
}
|
||||
@ -115,12 +145,21 @@ ensure_kompose() {
|
||||
}
|
||||
|
||||
list_k3d_clusters() {
|
||||
if k3d cluster list -o json >/dev/null 2>&1; then
|
||||
k3d cluster list -o json | python - <<'PY'
|
||||
local json=""
|
||||
if json="$(k3d cluster list -o json 2>/dev/null)"; then
|
||||
if [[ -n "$json" ]]; then
|
||||
if printf '%s' "$json" | python - <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
data = json.load(sys.stdin)
|
||||
raw = sys.stdin.read()
|
||||
if not raw.strip():
|
||||
sys.exit(1)
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
|
||||
items = data.get("items") if isinstance(data, dict) else data
|
||||
if not items:
|
||||
sys.exit(0)
|
||||
@ -129,7 +168,10 @@ for item in items:
|
||||
if name:
|
||||
print(name)
|
||||
PY
|
||||
return 0
|
||||
then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
k3d cluster list 2>/dev/null | awk 'NR>1 {print $1}'
|
||||
@ -182,6 +224,16 @@ parse_args() {
|
||||
MODE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-c|--config)
|
||||
PROLE_CFG_PATH="${2:-}"
|
||||
[[ -n "$PROLE_CFG_PATH" ]] || die "Missing value for $1"
|
||||
shift 2
|
||||
;;
|
||||
--config=*)
|
||||
PROLE_CFG_PATH="${1#*=}"
|
||||
[[ -n "$PROLE_CFG_PATH" ]] || die "Missing value for $1"
|
||||
shift
|
||||
;;
|
||||
--with-dev-helpers)
|
||||
USE_DEV_HELPERS="true"
|
||||
shift
|
||||
@ -318,6 +370,20 @@ ensure_image_artifact() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$DOCKER_IMPORT_DIR"
|
||||
|
||||
# Prefer buildx export to reliably produce single-arch tarballs with containerd-backed Docker
|
||||
if docker buildx version >/dev/null 2>&1; then
|
||||
log "Fetching image $img for platform $platform (buildx export)"
|
||||
if ! printf 'FROM %s\n' "$img" \
|
||||
| docker buildx build --pull --platform "$platform" -t "$platform_tag" \
|
||||
--output "type=docker,dest=$tar_path" - >/dev/null 2>&1; then
|
||||
die "Failed to export image $img ($platform) via buildx"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Fallback: traditional pull + save (may fail on some Docker/containerd combos)
|
||||
log "Fetching image $img for platform $platform"
|
||||
if ! docker pull --platform "$platform" "$img" >/dev/null 2>&1; then
|
||||
if docker image inspect "$img" >/dev/null 2>&1; then
|
||||
@ -327,10 +393,11 @@ ensure_image_artifact() {
|
||||
fi
|
||||
fi
|
||||
|
||||
docker tag "$img" "$platform_tag"
|
||||
mkdir -p "$DOCKER_IMPORT_DIR"
|
||||
docker tag "$img" "$platform_tag" >/dev/null 2>&1 || true
|
||||
log "Saving $img ($platform) to $tar_path"
|
||||
docker save -o "$tar_path" "$platform_tag" >/dev/null
|
||||
if ! docker save -o "$tar_path" "$platform_tag" >/dev/null 2>&1; then
|
||||
die "Failed to save image $img ($platform) to $tar_path"
|
||||
fi
|
||||
}
|
||||
|
||||
load_image_for_platform() {
|
||||
@ -411,14 +478,12 @@ prefetch_k3d_images() {
|
||||
load_image_for_platform "$img" "$deploy_platform"
|
||||
done
|
||||
|
||||
log "Importing images into k3d"
|
||||
for img in $images; do
|
||||
if [[ -n "${K3D_CLUSTER_NAME:-}" ]]; then
|
||||
k3d image import "$img" -c "$K3D_CLUSTER_NAME"
|
||||
else
|
||||
k3d image import "$img"
|
||||
fi
|
||||
done
|
||||
# shellcheck disable=SC2086
|
||||
if [[ -n "${K3D_CLUSTER_NAME:-}" ]]; then
|
||||
k3d image import $images -c "$K3D_CLUSTER_NAME"
|
||||
else
|
||||
k3d image import $images
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_prole_db_namespace() {
|
||||
@ -426,6 +491,11 @@ resolve_prole_db_namespace() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n "${NAMESPACE:-}" ]]; then
|
||||
PROLE_DB_NAMESPACE="$NAMESPACE"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if kubectl get namespace prole >/dev/null 2>&1; then
|
||||
PROLE_DB_NAMESPACE="prole"
|
||||
else
|
||||
@ -522,6 +592,40 @@ generate_k8s_manifests() {
|
||||
patch_db_deployment_port
|
||||
write_supabase_postgres_service
|
||||
write_db_alias_service
|
||||
sanitize_container_names
|
||||
}
|
||||
|
||||
sanitize_container_names() {
|
||||
log "Sanitizing container names in manifests..."
|
||||
local files
|
||||
files=("$SUPABASE_K8S_DIR"/*-deployment.yaml)
|
||||
for file in "${files[@]}"; do
|
||||
[[ -f "$file" ]] || continue
|
||||
# Replace dots with hyphens in container names (spec.template.spec.containers[].name)
|
||||
python - "$file" <<'PY'
|
||||
import sys
|
||||
import re
|
||||
|
||||
path = sys.argv[1]
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
def replace_dots(match):
|
||||
prefix = match.group(1)
|
||||
name = match.group(2)
|
||||
if '.' in name:
|
||||
return prefix + name.replace('.', '-')
|
||||
return match.group(0)
|
||||
|
||||
# Match 'name: some.container.name' with at least 4 spaces of indentation
|
||||
# This helps avoid metadata.name which usually has 2 spaces.
|
||||
new_content = re.sub(r'(\s{4,}name:\s+)([^\n]+)', replace_dots, content)
|
||||
|
||||
if new_content != content:
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
f.write(new_content)
|
||||
PY
|
||||
done
|
||||
}
|
||||
|
||||
ensure_supabase_namespace() {
|
||||
@ -541,11 +645,18 @@ force_reset_supabase_namespace() {
|
||||
python - <<'PY' | kubectl replace --raw "/api/v1/namespaces/supabase/finalize" -f - >/dev/null 2>&1 || true
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
raw = subprocess.check_output(["kubectl", "get", "namespace", "supabase", "-o", "json"])
|
||||
data = json.loads(raw)
|
||||
data["spec"]["finalizers"] = []
|
||||
print(json.dumps(data))
|
||||
try:
|
||||
raw = subprocess.check_output(["kubectl", "get", "namespace", "supabase", "-o", "json"])
|
||||
if not raw:
|
||||
sys.exit(0)
|
||||
data = json.loads(raw)
|
||||
if "spec" in data:
|
||||
data["spec"]["finalizers"] = []
|
||||
print(json.dumps(data))
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
PY
|
||||
kubectl wait --for=delete namespace/supabase --timeout=120s >/dev/null 2>&1 || true
|
||||
fi
|
||||
@ -592,8 +703,45 @@ run_k3d() {
|
||||
log "Access Studio via Kong proxy (check ingress/service in 'supabase' namespace)."
|
||||
}
|
||||
|
||||
run_k8s() {
|
||||
ensure_repo
|
||||
|
||||
log "Prole::Supabase k8s deploy (Kubernetes - generic/containerd)"
|
||||
log "Repo: $SUPABASE_DIR"
|
||||
|
||||
require_cmd kubectl
|
||||
kubectl cluster-info >/dev/null 2>&1 || die "Kubernetes cluster not reachable"
|
||||
|
||||
if [[ "$FORCE" == "true" ]]; then
|
||||
force_reset_supabase_namespace
|
||||
else
|
||||
ensure_supabase_namespace
|
||||
fi
|
||||
|
||||
export SUPABASE_HOME="$SUPABASE_DIR"
|
||||
if [[ "$USE_DEV_HELPERS" == "true" ]]; then
|
||||
export SUPABASE_USE_DEV_COMPOSE=1
|
||||
fi
|
||||
|
||||
# For generic k8s/k3s clusters (containerd), let nodes pull appropriate arch images directly
|
||||
# Optionally, a future enhancement could push pre-fetched images to an internal registry.
|
||||
generate_k8s_manifests
|
||||
|
||||
if [[ -f "$SUPABASE_K8S_DIR/supabase-namespace.yaml" ]]; then
|
||||
kubectl apply -f "$SUPABASE_K8S_DIR/supabase-namespace.yaml"
|
||||
fi
|
||||
|
||||
log "Applying Supabase manifests from $SUPABASE_K8S_DIR"
|
||||
kubectl apply -f "$SUPABASE_K8S_DIR"
|
||||
|
||||
log "Deployment complete."
|
||||
log "Access Studio via Kong proxy (check ingress/service in 'supabase' namespace)."
|
||||
}
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
load_prole_cfg
|
||||
apply_defaults
|
||||
|
||||
if [[ -z "$MODE" ]]; then
|
||||
usage
|
||||
@ -607,6 +755,9 @@ main() {
|
||||
k3d)
|
||||
run_k3d
|
||||
;;
|
||||
k8s)
|
||||
run_k8s
|
||||
;;
|
||||
*)
|
||||
die "Unsupported mode: $MODE"
|
||||
;;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user