prole/tools/k3s-cluster-rename.sh
chrisfu 7b242e88f5 Persist k3s context rename and scrub service config paths
Update conf/service/prole.cfg to persist the k3s kube context as knoe.dev.prole.org and replace host-specific absolute paths with $HOME-based paths for portability.

Include all pending project changes: local_user role package and screenrc provisioning updates, plus tools/k3s-cluster-rename.sh and tests/etc/test_k3s_cluster_rename.sh.

Co-authored-by: Junie <junie@jetbrains.com>
2026-03-28 11:23:19 -07:00

229 lines
6.6 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
OLD_CONTEXT_DEFAULT="prole-k3s"
NEW_CONTEXT_DEFAULT="knoe.dev.prole.org"
usage() {
cat <<'EOF'
Usage: k3s-cluster-rename.sh [--kubeconfig PATH] [--from OLD] [--to NEW] [--no-backup]
Rename/repair a k3s kubeconfig context so kubectx can switch to the new name.
Defaults:
--from prole-k3s
--to knoe.dev.prole.org
Notes:
- If --from is missing but kubeconfig has exactly one context, that context is used.
- Default kubeconfig target is the active shell config source:
- first path in $KUBECONFIG (if set)
- otherwise ~/.kube/config
- By default a timestamped backup is written next to kubeconfig.
EOF
}
log() {
printf '%s\n' "$*" >&2
}
die() {
log "ERROR: $*"
exit 1
}
require_cmd() {
local cmd="$1"
command -v "$cmd" >/dev/null 2>&1 || die "required command not found: $cmd"
}
KUBECONFIG_PATH=""
OLD_CONTEXT="$OLD_CONTEXT_DEFAULT"
NEW_CONTEXT="$NEW_CONTEXT_DEFAULT"
CREATE_BACKUP=1
while [[ $# -gt 0 ]]; do
case "$1" in
--kubeconfig)
[[ $# -ge 2 ]] || die "missing value for --kubeconfig"
KUBECONFIG_PATH="$2"
shift 2
;;
--from)
[[ $# -ge 2 ]] || die "missing value for --from"
OLD_CONTEXT="$2"
shift 2
;;
--to)
[[ $# -ge 2 ]] || die "missing value for --to"
NEW_CONTEXT="$2"
shift 2
;;
--no-backup)
CREATE_BACKUP=0
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown argument: $1"
;;
esac
done
require_cmd kubectl
require_cmd python3
if [[ -z "$KUBECONFIG_PATH" ]]; then
if [[ -n "${KUBECONFIG:-}" ]]; then
KUBECONFIG_PATH="${KUBECONFIG%%:*}"
[[ -n "$KUBECONFIG_PATH" ]] || KUBECONFIG_PATH="${HOME}/.kube/config"
else
KUBECONFIG_PATH="${HOME}/.kube/config"
fi
fi
[[ -n "$KUBECONFIG_PATH" ]] || die "could not resolve kubeconfig path"
[[ -f "$KUBECONFIG_PATH" ]] || die "kubeconfig not found: $KUBECONFIG_PATH"
[[ -r "$KUBECONFIG_PATH" ]] || die "kubeconfig is not readable: $KUBECONFIG_PATH"
[[ -w "$KUBECONFIG_PATH" ]] || die "kubeconfig is not writable: $KUBECONFIG_PATH"
tmp_input=$(mktemp "${TMPDIR:-/tmp}/k3s-cluster-rename-input.XXXXXX.json")
tmp_output=$(mktemp "${TMPDIR:-/tmp}/k3s-cluster-rename-output.XXXXXX.json")
cleanup() {
rm -f "$tmp_input" "$tmp_output"
}
trap cleanup EXIT
KUBECONFIG="$KUBECONFIG_PATH" kubectl config view --raw -o json >"$tmp_input"
python3 - "$OLD_CONTEXT" "$NEW_CONTEXT" "$tmp_input" "$tmp_output" <<'PY'
import json
import sys
source_name, target_name, in_path, out_path = sys.argv[1:5]
with open(in_path, "r", encoding="utf-8") as fh:
data = json.load(fh)
contexts = data.get("contexts") or []
context_names = [c.get("name", "") for c in contexts if isinstance(c, dict)]
def get_context_obj(name: str):
for ctx in contexts:
if isinstance(ctx, dict) and ctx.get("name") == name:
return ctx
return None
primary_name = None
if source_name in context_names:
primary_name = source_name
elif target_name in context_names:
primary_name = target_name
elif len(context_names) == 1:
primary_name = context_names[0]
sys.stderr.write(
f"INFO: context '{source_name}' not found, using only context '{primary_name}' for repair.\n"
)
else:
sys.stderr.write(
f"ERROR: context '{source_name}' not found and auto-repair is ambiguous (contexts={context_names}).\n"
)
sys.exit(2)
target_exists = target_name in context_names
if not target_exists and primary_name != target_name:
primary_ctx = get_context_obj(primary_name)
if primary_ctx is None:
sys.stderr.write(f"ERROR: could not locate primary context '{primary_name}'.\n")
sys.exit(2)
primary_ctx["name"] = target_name
if data.get("current-context") == primary_name:
data["current-context"] = target_name
canonical_ctx = get_context_obj(target_name)
if canonical_ctx is None:
canonical_ctx = get_context_obj(primary_name)
if canonical_ctx is None:
sys.stderr.write("ERROR: failed to identify context to normalize.\n")
sys.exit(2)
ctx_body = canonical_ctx.get("context") or {}
old_cluster = ctx_body.get("cluster")
old_user = ctx_body.get("user")
clusters = data.get("clusters") or []
users = data.get("users") or []
contexts = data.get("contexts") or []
cluster_names = {c.get("name") for c in clusters if isinstance(c, dict)}
user_names = {u.get("name") for u in users if isinstance(u, dict)}
if old_cluster and old_cluster != target_name and target_name not in cluster_names:
for cluster in clusters:
if isinstance(cluster, dict) and cluster.get("name") == old_cluster:
cluster["name"] = target_name
break
for ctx in contexts:
body = ctx.get("context") if isinstance(ctx, dict) else None
if isinstance(body, dict) and body.get("cluster") == old_cluster:
body["cluster"] = target_name
elif old_cluster:
ctx_body["cluster"] = target_name if target_name in cluster_names else old_cluster
if old_user and old_user != target_name and target_name not in user_names:
for user in users:
if isinstance(user, dict) and user.get("name") == old_user:
user["name"] = target_name
break
for ctx in contexts:
body = ctx.get("context") if isinstance(ctx, dict) else None
if isinstance(body, dict) and body.get("user") == old_user:
body["user"] = target_name
elif old_user:
ctx_body["user"] = target_name if target_name in user_names else old_user
data["current-context"] = target_name
with open(out_path, "w", encoding="utf-8") as fh:
json.dump(data, fh, indent=2)
fh.write("\n")
PY
found_context=0
while IFS= read -r ctx_name; do
if [[ "$ctx_name" == "$NEW_CONTEXT" ]]; then
found_context=1
break
fi
done < <(KUBECONFIG="$tmp_output" kubectl config get-contexts -o name)
[[ "$found_context" -eq 1 ]] || die "rename failed: context '$NEW_CONTEXT' not present in transformed kubeconfig"
if [[ "$CREATE_BACKUP" -eq 1 ]]; then
backup_path="${KUBECONFIG_PATH}.bak.$(date +%Y%m%d%H%M%S)"
cp "$KUBECONFIG_PATH" "$backup_path"
log "Backup written: $backup_path"
fi
cp "$tmp_output" "$KUBECONFIG_PATH"
chmod 600 "$KUBECONFIG_PATH" 2>/dev/null || true
log "Kubeconfig updated: $KUBECONFIG_PATH"
log "Context/cluster/user target: $NEW_CONTEXT"
if command -v kubectx >/dev/null 2>&1; then
if KUBECONFIG="$KUBECONFIG_PATH" kubectx "$NEW_CONTEXT" >/dev/null 2>&1; then
log "kubectx switch verification succeeded for '$NEW_CONTEXT' (using KUBECONFIG=$KUBECONFIG_PATH)."
else
die "kubectx could not switch to '$NEW_CONTEXT' for kubeconfig '$KUBECONFIG_PATH'"
fi
else
log "kubectx not found; skipped kubectx switch verification."
fi
log "Done."