#!/usr/bin/env bash set -euo pipefail HETZNER_CSI_VERSION="${HETZNER_CSI_VERSION:-2.21.2}" HETZNER_CSI_STORAGE_CLASS="${HETZNER_CSI_STORAGE_CLASS:-hcloud-volumes}" HETZNER_CSI_RECLAIM_POLICY="${HETZNER_CSI_RECLAIM_POLICY:-Retain}" HETZNER_CSI_DEFAULT_FS="${HETZNER_CSI_DEFAULT_FS:-ext4}" HETZNER_CSI_TOKEN_SECRET="${HETZNER_CSI_TOKEN_SECRET:-hcloud}" HETZNER_CSI_ENABLE_SNAPSHOT_CLASS="${HETZNER_CSI_ENABLE_SNAPSHOT_CLASS:-false}" # Host kubelet data dir the csi-node DaemonSet binds. Default is the kubeadm path; # k0s uses /var/lib/data/k0s/kubelet. The upstream manifest hardcodes /var/lib/kubelet, # so we rewrite it below when the target distro differs — otherwise NodeStage mounts # into a path kubelet never reads and every PVC silently falls back to local disk. HETZNER_CSI_KUBELET_DIR="${HETZNER_CSI_KUBELET_DIR:-/var/lib/kubelet}" # external-snapshotter version compatible with Hetzner CSI 2.x SNAPSHOTTER_VERSION="v8.2.0" MANIFEST_URL="https://raw.githubusercontent.com/hetznercloud/csi-driver/v${HETZNER_CSI_VERSION}/deploy/kubernetes/hcloud-csi.yml" echo "=== hetzner-csi: checking prerequisites ===" # Resolve kubectl: k0s kubectl, standard kubectl 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 # Require the HCLOUD_TOKEN secret to exist before deploying — it must be # provisioned out-of-band (SOPS-managed) so we never store the token in plain text. if ! kubectl -n kube-system get secret "${HETZNER_CSI_TOKEN_SECRET}" &>/dev/null; then echo "ERROR: Secret '${HETZNER_CSI_TOKEN_SECRET}' not found in kube-system." >&2 echo " Create it first: kubectl -n kube-system create secret generic ${HETZNER_CSI_TOKEN_SECRET} --from-literal=token=" >&2 exit 1 fi echo "=== hetzner-csi: deploying v${HETZNER_CSI_VERSION} ===" # Apply official manifest EXCLUDING StorageClass (we manage it separately with custom reclaimPolicy). # Rewrite the hardcoded /var/lib/kubelet csi-node paths to the target distro's kubelet dir # (no-op when default). /var/lib/kubelet appears only in the node DaemonSet (hostPaths + # --kubelet-registration-path), so a global substitution is safe. '#' delimiter because the # replacement contains slashes. if [ "${HETZNER_CSI_KUBELET_DIR}" != "/var/lib/kubelet" ]; then echo "=== hetzner-csi: rewriting csi-node kubelet dir → ${HETZNER_CSI_KUBELET_DIR} ===" fi curl -sL "${MANIFEST_URL}" \ | awk 'BEGIN{skip=0} /^kind: StorageClass/{skip=1} /^---/{if(skip){skip=0;next}} {if(!skip)print}' \ | sed "s#/var/lib/kubelet#${HETZNER_CSI_KUBELET_DIR}#g" \ | kubectl apply -f - # Create or recreate StorageClass with desired name and reclaimPolicy CURRENT_POLICY="$(kubectl get storageclass "${HETZNER_CSI_STORAGE_CLASS}" -o jsonpath='{.reclaimPolicy}' 2>/dev/null || echo "")" if [ -z "${CURRENT_POLICY}" ] || [ "${CURRENT_POLICY}" != "${HETZNER_CSI_RECLAIM_POLICY}" ]; then [ -n "${CURRENT_POLICY}" ] && kubectl delete storageclass "${HETZNER_CSI_STORAGE_CLASS}" 2>/dev/null || true echo "=== hetzner-csi: creating StorageClass ${HETZNER_CSI_STORAGE_CLASS} (reclaimPolicy=${HETZNER_CSI_RECLAIM_POLICY}) ===" kubectl apply -f - <