#!/usr/bin/env bash set -euo pipefail # Source the rendered env file from the bundle — the runner invokes this under # `sudo bash` which strips the GARAGE_* environment, so without this every var # below falls back to its default (empty node_selector, empty buckets → no keys). # Mirrors install-loki.sh. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" [ -f "${SCRIPT_DIR}/env-garage" ] && source "${SCRIPT_DIR}/env-garage" GARAGE_VERSION="${GARAGE_VERSION:-v2.3.0}" GARAGE_NAMESPACE="${GARAGE_NAMESPACE:-garage}" GARAGE_REPLICAS="${GARAGE_REPLICAS:-1}" GARAGE_REPLICATION_FACTOR="${GARAGE_REPLICATION_FACTOR:-1}" GARAGE_STORAGE_CLASS="${GARAGE_STORAGE_CLASS:-hcloud-volumes}" GARAGE_DATA_VOLUME_SIZE="${GARAGE_DATA_VOLUME_SIZE:-10Gi}" GARAGE_S3_API_PORT="${GARAGE_S3_API_PORT:-3900}" GARAGE_ADMIN_API_PORT="${GARAGE_ADMIN_API_PORT:-3903}" GARAGE_RPC_PORT="${GARAGE_RPC_PORT:-3901}" GARAGE_S3_REGION="${GARAGE_S3_REGION:-garage}" GARAGE_SECRET_NAME="${GARAGE_SECRET_NAME:-garage-rpc-admin}" # NOTE: do NOT write ${VAR:-{}} — bash parses the first '}' as the end of the # expansion, appending a stray '}' to a non-empty value ("{...}}", invalid YAML). GARAGE_NODE_SELECTOR_JSON="${GARAGE_NODE_SELECTOR_JSON:-}" [ -n "${GARAGE_NODE_SELECTOR_JSON}" ] || GARAGE_NODE_SELECTOR_JSON="{}" GARAGE_BUCKETS="${GARAGE_BUCKETS:-}" IMAGE="dxflrs/garage:${GARAGE_VERSION}" echo "=== garage: checking prerequisites ===" if command -v k0s &>/dev/null && k0s kubectl version --client=true &>/dev/null; then kubectl() { k0s kubectl "$@"; } elif ! command -v kubectl &>/dev/null; then echo "ERROR: neither kubectl nor k0s found" >&2 exit 1 fi kubectl create namespace "${GARAGE_NAMESPACE}" --dry-run=client -o yaml | kubectl apply -f - if ! kubectl -n "${GARAGE_NAMESPACE}" get secret "${GARAGE_SECRET_NAME}" &>/dev/null; then echo "ERROR: Secret '${GARAGE_SECRET_NAME}' not found in ${GARAGE_NAMESPACE}." >&2 echo " Provisioned out-of-band by the 'prepare' step from SOPS — rerun prepare or check the .sops.yaml." >&2 exit 1 fi RPC_SECRET="$(kubectl -n "${GARAGE_NAMESPACE}" get secret "${GARAGE_SECRET_NAME}" -o jsonpath='{.data.rpc_secret}' | base64 -d)" ADMIN_TOKEN="$(kubectl -n "${GARAGE_NAMESPACE}" get secret "${GARAGE_SECRET_NAME}" -o jsonpath='{.data.admin_token}' | base64 -d)" echo "=== garage: rendering config secret ===" GARAGE_TOML="$(cat < "${GARAGE_MANIFEST}" </dev/null || true)" [ "${phase}" = "Running" ] && break sleep 3 done echo "=== garage: bootstrapping cluster layout (idempotent) ===" # `garage node id -q` prints "@"; `layout assign` wants the bare # hex id (the @addr suffix makes it reject the node). Strip it. NODE_ID="$(kubectl -n "${GARAGE_NAMESPACE}" exec garage-0 -- /garage node id -q 2>/dev/null | tr -d '\r' | cut -d@ -f1)" ALREADY_APPLIED="$(kubectl -n "${GARAGE_NAMESPACE}" exec garage-0 -- /garage layout show 2>/dev/null | grep -c "^Current cluster layout version: [1-9]" || true)" if [ "${ALREADY_APPLIED}" = "0" ]; then # `layout assign -c` parses capacity with bytesize (garage >=0.9): "10G" is valid, # the K8s-style "10Gi" is not. Strip the trailing 'i' from the PVC-format size. GARAGE_LAYOUT_CAPACITY="${GARAGE_DATA_VOLUME_SIZE%i}" kubectl -n "${GARAGE_NAMESPACE}" exec garage-0 -- /garage layout assign -z fsn1 -c "${GARAGE_LAYOUT_CAPACITY}" "${NODE_ID}" kubectl -n "${GARAGE_NAMESPACE}" exec garage-0 -- /garage layout apply --version 1 echo "=== garage: layout applied ===" else echo "=== garage: layout already applied — skipping ===" fi # Layout assigned → node has a role → /health flips to 200 and the pod goes Ready. # Confirm before bucket ops (garage returns "Layout not ready" until it propagates). kubectl -n "${GARAGE_NAMESPACE}" rollout status statefulset/garage --timeout=120s echo "=== garage: reconciling buckets (idempotent) ===" # GARAGE_BUCKETS is space-separated "name:key_name" pairs (rendered by # env-garage.j2). Pure bash parsing — runs on cluster nodes with no extra tooling. for pair in ${GARAGE_BUCKETS}; do bucket_name="${pair%%:*}" key_name="${pair##*:}" [ -z "${bucket_name}" ] && continue # `garage bucket list` prints a table ("ID date "), so the bucket name # is a word in a column — match with -w, NOT -x (whole-line, never matches → # CreateBucket 409 BucketAlreadyExists on every reconcile). if ! kubectl -n "${GARAGE_NAMESPACE}" exec garage-0 -- /garage bucket list 2>/dev/null | grep -qw "${bucket_name}"; then kubectl -n "${GARAGE_NAMESPACE}" exec garage-0 -- /garage bucket create "${bucket_name}" fi if ! kubectl -n "${GARAGE_NAMESPACE}" exec garage-0 -- /garage key list 2>/dev/null | grep -qw "${key_name}"; then echo "=== garage: creating key '${key_name}' for bucket '${bucket_name}' — CAPTURE THESE CREDENTIALS NOW, store in SOPS ===" kubectl -n "${GARAGE_NAMESPACE}" exec garage-0 -- /garage key create "${key_name}" kubectl -n "${GARAGE_NAMESPACE}" exec garage-0 -- /garage bucket allow --read --write --key "${key_name}" "${bucket_name}" else echo "=== garage: key '${key_name}' already exists — not reprinting credentials ===" fi done # Report the real S3 endpoint consumers must use. The cluster DNS domain is NOT # always "cluster.local" — kubeadm can set --cluster-domain (libre-wuji uses # "libre-wuji.local"). Detect it from the coredns Corefile so the printed # endpoint is correct per cluster; fall back to cluster.local if undetectable. CLUSTER_DOMAIN="$(kubectl -n kube-system get cm coredns -o jsonpath='{.data.Corefile}' 2>/dev/null | grep -oE 'kubernetes[[:space:]]+[^[:space:]]+' | awk '{print $2}' | head -1)" CLUSTER_DOMAIN="${CLUSTER_DOMAIN:-cluster.local}" echo "=== garage: ready (namespace=${GARAGE_NAMESPACE}, replicas=${GARAGE_REPLICAS}, s3_region=${GARAGE_S3_REGION}, s3_endpoint=http://garage.${GARAGE_NAMESPACE}.svc.${CLUSTER_DOMAIN}:${GARAGE_S3_API_PORT}) ==="