Initial import: phase docs, configs, Claude conventions

Phase 1-3 runbooks and design notes, version pins, config templates,
project-history conventions, cluster-inspector read-only credentials setup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 00:58:52 +02:00
co-authored by Claude Opus 5.5
commit 5fd998c5f9
26 changed files with 1124 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
---
name: cluster-inspector
description: Read-only inspection and triage of the lab management cluster (CAPI, CAPMOX, IPAM, cluster-autoscaler) and workload clusters. Use when a cluster, Machine, ProxmoxMachine, node or autoscaling behaviour needs diagnosing. Never mutates anything.
tools: Bash, Read, Grep
---
You diagnose Cluster API / cluster-autoscaler issues in the lab. You are strictly read-only:
only `kubectl get/describe/logs/events/explain`, `clusterctl describe`, `kubectl auth can-i`,
and read-only Proxmox commands (`pvesh get`, `qm config`, `qm list`, `qm guest cmd … network-get-interfaces`).
Never run apply/create/delete/patch/edit/scale/annotate/label/cordon/drain, `qm set/start/stop/destroy`,
`pveum` changes, or anything that writes. If a fix needs a mutation, output the exact command
for the owner to run.
Credentials — use ONLY the read-only kubeconfigs, never ~/.kube/config or any admin kubeconfig:
- Files: `$(git rev-parse --show-toplevel)/.claude/cluster-inspector/kubeconfigs/<cluster>.kubeconfig`,
context `<cluster>-ro` (e.g. `lab-mgmt`, `lab1`). List the directory to see which clusters exist.
- Always pass it explicitly: `KUBECONFIG=<file> kubectl ...` (or `--kubeconfig <file>`).
- If the file is missing, or kubectl returns 401 (expired token), stop and tell the owner to run
`.claude/cluster-inspector/make-kubeconfig.sh <cluster> ...` (see .claude/cluster-inspector/README.md).
- Secrets are intentionally not readable; a 403 on secrets is expected, don't work around it.
Triage order:
1. `clusterctl describe cluster <name> --show-conditions all`
2. Objects + conditions: `kubectl get cluster,kcp,md,ms,machines,proxmoxmachines -A -o wide`,
`kubectl describe` the first non-Ready object.
3. Controller logs: `capi-system/capi-controller-manager`, `capmox-system/capmox-controller-manager`,
`capi-kubeadm-*-system`, IPAM (`kubectl get ipaddressclaims,ipaddresses -A`).
4. Node side: Node vs Machine providerID match
(`kubectl get nodes -o custom-columns=N:.metadata.name,P:.spec.providerID`), kubelet/cloud-init
status if SSH access is given.
5. Autoscaler: CA logs (node group discovery, scale decisions), pending pod events
(`TriggeredScaleUp` / `NotTriggerScaleUp`), MachineDeployment min/max + capacity annotations,
whether `spec.replicas` is being fought by a re-apply.
Known failure → fix mappings live in docs/phase-1-proxmox-capi.md and docs/image-builder-proxmox.md;
check them first. Report: root cause (with evidence lines), fix command(s), and anything unverified.
+26
View File
@@ -0,0 +1,26 @@
---
name: upstream-researcher
description: Answers "does upstream project X (CAPI providers, cluster-autoscaler, image-builder, Kamaji, KubeVirt, packer plugins) support Y / how does it behave / which versions are compatible" from the tagged release's source code, not READMEs. Use for provider evaluations, version-compatibility questions and root-causing upstream behaviour. Read-only.
tools: Bash, Read, Grep, Glob, WebFetch, WebSearch
---
You research upstream Kubernetes ecosystem projects for a home-lab Cluster API project
(see CLAUDE.md and docs/versions.md for the pinned stack). You never modify the repository.
Method:
1. Identify the exact module path and the release in question (pinned version from docs/versions.md,
or latest if asked). Latest: `curl -sS https://proxy.golang.org/<module>/@latest`;
all tags: `git ls-remote --tags <repo-url>` (works when the GitHub API is blocked).
2. Fetch the source of that exact tag into a temp dir, e.g.
`curl -sSL -o src.zip https://proxy.golang.org/<module>/@v/<ver>.zip && unzip -q src.zip -d src`
(non-Go projects: `git clone --depth 1 --branch <tag>`).
3. Answer from code: API types (`api/*/…_types.go`), webhooks, controllers, `templates/`,
`metadata.yaml` (contract), `go.mod` (CAPI / k8s versions), Makefile / hack scripts.
4. For compatibility: compare the CAPI version in go.mod and the contract in metadata.yaml with the
pinned CAPI core; check the project's own compatibility table.
Report format (concise, SRE audience):
- Answer in 1–3 sentences.
- Evidence: file path + line or a short code excerpt from the exact tag, plus the version checked.
- Caveats / version constraints relevant to our pinned stack.
- If something could not be verified, say so explicitly — never fill gaps from memory.
+69
View File
@@ -0,0 +1,69 @@
# cluster-inspector: credentials setup
The `cluster-inspector` agent (`.claude/agents/cluster-inspector.md`) only ever talks to clusters
through the read-only kubeconfigs in `kubeconfigs/` here. They're gitignored and minted by
`make-kubeconfig.sh` from a short-lived ServiceAccount token.
| File | Purpose |
| --- | --- |
| `rbac.yaml` | ServiceAccount `kube-system/readonly-inspector` + `view` + read on CAPI CRDs and pod logs (no Secrets) |
| `make-kubeconfig.sh` | applies RBAC (once), mints a TokenRequest token, writes `kubeconfigs/<name>.kubeconfig`, runs `can-i` checks |
| `kubeconfigs/` | output, **gitignored**; one file per cluster, context `<name>-ro` |
## 1. Management cluster (after phase 1 step 2)
k3s writes `server: https://127.0.0.1:6443`, so pass the VM address. k3s puts node IPs into the
API cert SANs, so the IP works; add `--tls-san` for a DNS name.
```bash
scp ubuntu@<mgmt-vm>:/etc/rancher/k3s/k3s.yaml /tmp/lab-mgmt-admin.yaml # or use your existing admin kubeconfig
.claude/cluster-inspector/make-kubeconfig.sh lab-mgmt \
--admin-kubeconfig /tmp/lab-mgmt-admin.yaml \
--server https://<mgmt-vm-ip>:6443 \
--apply-rbac
rm /tmp/lab-mgmt-admin.yaml
```
## 2. Workload clusters (after phase 1 step 3)
```bash
clusterctl get kubeconfig lab1 > /tmp/lab1-admin.yaml # run against the mgmt cluster (admin)
.claude/cluster-inspector/make-kubeconfig.sh lab1 --admin-kubeconfig /tmp/lab1-admin.yaml --apply-rbac
rm /tmp/lab1-admin.yaml
```
## Expected verification output
```
list pods yes
get machines.cluster.x-k8s.io yes # "no" on workload clusters (no CAPI CRDs), fine
delete machines.cluster.x-k8s.io no
patch machinedeployments.cluster.x-k8s.io no
get secrets no
```
Anything else means the RBAC is wrong. Don't use the agent until it matches.
## Refresh / revoke
- **Refresh:** re-run the same command without `--apply-rbac`. The default lifetime is 7 days
(`--duration`); the API server may cap it (`--service-account-max-token-expiration`).
- **Revoke everything:** `kubectl delete sa -n kube-system readonly-inspector` (as admin). That
invalidates every minted token; re-apply `rbac.yaml` to start over.
## Making read-only the default for this repo (recommended)
RBAC only protects you when the read-only kubeconfig is actually used. Your admin
`~/.kube/config` is still reachable from any shell. To make Claude Code sessions in this repo
default to the read-only files, add this to `.claude/settings.local.json` (personal, not committed):
```json
{
"env": {
"KUBECONFIG": "/Users/jan.novak/srv/go/cluster-autoscaler-with-capi/.claude/cluster-inspector/kubeconfigs/lab-mgmt.kubeconfig:/Users/jan.novak/srv/go/cluster-autoscaler-with-capi/.claude/cluster-inspector/kubeconfigs/lab1.kubeconfig"
}
}
```
Then plain `kubectl --context lab-mgmt-ro …` / `--context lab1-ro …` works everywhere in the
session, and mutations fail on RBAC even if something tries them.
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Mint a read-only kubeconfig for the cluster-inspector agent.
#
# Usage: make-kubeconfig.sh <name> [options]
# <name> cluster name, e.g. lab-mgmt or lab1 -> kubeconfigs/<name>.kubeconfig
# --admin-kubeconfig PATH admin kubeconfig used to apply RBAC / mint the token (default: $KUBECONFIG or ~/.kube/config)
# --context CTX context inside the admin kubeconfig
# --server URL override API server URL (k3s.yaml says https://127.0.0.1:6443)
# --duration DUR token lifetime (default 168h); re-run the script to refresh
# --apply-rbac kubectl apply rbac.yaml first (needed once per cluster)
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
name=""; admin="${KUBECONFIG:-$HOME/.kube/config}"; ctx=""; server=""; duration="168h"; apply=0
while [[ $# -gt 0 ]]; do
case "$1" in
--admin-kubeconfig) admin="$2"; shift 2 ;;
--context) ctx="$2"; shift 2 ;;
--server) server="$2"; shift 2 ;;
--duration) duration="$2"; shift 2 ;;
--apply-rbac) apply=1; shift ;;
-h|--help) sed -n '2,10p' "$0"; exit 0 ;;
-*) echo "unknown option $1" >&2; exit 1 ;;
*) name="$1"; shift ;;
esac
done
[[ -n "$name" ]] || { sed -n '2,10p' "$0" >&2; exit 1; }
k=(kubectl --kubeconfig "$admin")
[[ -n "$ctx" ]] && k+=(--context "$ctx")
if (( apply )); then "${k[@]}" apply -f "$here/rbac.yaml"; fi
token="$("${k[@]}" -n kube-system create token readonly-inspector --duration "$duration")"
[[ -n "$server" ]] || server="$("${k[@]}" config view --raw --minify --flatten -o jsonpath='{.clusters[0].cluster.server}')"
ca="$("${k[@]}" config view --raw --minify --flatten -o jsonpath='{.clusters[0].cluster.certificate-authority-data}')"
[[ -n "$ca" ]] || { echo "no certificate-authority-data in admin kubeconfig" >&2; exit 1; }
mkdir -p "$here/kubeconfigs"
out="$here/kubeconfigs/$name.kubeconfig"
umask 077
cat > "$out" <<EOF
apiVersion: v1
kind: Config
clusters:
- name: $name
cluster:
server: $server
certificate-authority-data: $ca
users:
- name: $name-readonly-inspector
user:
token: $token
contexts:
- name: $name-ro
context:
cluster: $name
user: $name-readonly-inspector
current-context: $name-ro
EOF
echo "wrote $out (context $name-ro, server $server, token valid $duration)"
echo "verification (expect yes, yes, no, no, no; CAPI rows say no on workload clusters):"
for check in "list pods" "get machines.cluster.x-k8s.io" "delete machines.cluster.x-k8s.io" \
"patch machinedeployments.cluster.x-k8s.io" "get secrets"; do
printf ' %-45s %s\n' "$check" "$(KUBECONFIG="$out" kubectl auth can-i $check -A 2>/dev/null || true)"
done
+43
View File
@@ -0,0 +1,43 @@
# Read-only identity for the cluster-inspector agent.
# Apply to the management cluster and to every workload cluster the agent should inspect
# (CAPI rules are simply unused on workload clusters).
# Built-in `view` excludes Secrets; capi-readonly adds read access to CAPI CRDs and pod logs.
# Tokens are short-lived TokenRequest tokens minted by make-kubeconfig.sh — no Secret-based token.
apiVersion: v1
kind: ServiceAccount
metadata:
name: readonly-inspector
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: capi-readonly
rules:
- apiGroups:
- cluster.x-k8s.io
- infrastructure.cluster.x-k8s.io
- controlplane.cluster.x-k8s.io
- bootstrap.cluster.x-k8s.io
- ipam.cluster.x-k8s.io
- addons.cluster.x-k8s.io
- clusterctl.cluster.x-k8s.io
resources: ["*"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: readonly-inspector-view
roleRef: { apiGroup: rbac.authorization.k8s.io, kind: ClusterRole, name: view }
subjects: [{ kind: ServiceAccount, name: readonly-inspector, namespace: kube-system }]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: readonly-inspector-capi
roleRef: { apiGroup: rbac.authorization.k8s.io, kind: ClusterRole, name: capi-readonly }
subjects: [{ kind: ServiceAccount, name: readonly-inspector, namespace: kube-system }]
+23
View File
@@ -0,0 +1,23 @@
---
description: Commit current changes and push to the remote
---
Commit the current working-tree changes and push them.
1. Run `git status` and `git diff` to see what will be committed.
2. Check the staged content for secrets: Proxmox token secrets, kubeconfig contents, anything
`.gitignore` would have caught by name but that was pasted into a doc. Stop and report if found.
3. Check the current branch (`git branch --show-current`). Docs, configs and history files may go
straight to `main`. If on `main` and the diff contains phase-3 Go code or something the user
called a feature, stop and tell the user to branch first (CLAUDE.md "Git").
4. If an approved plan is in progress, make sure its execution log in `docs/plans-executions/` is
updated and its checklist ticked for the steps included in this commit.
5. Stage the changed files (`git add -A` is fine here).
6. Commit with a short message inferred from the diff, ending with the `Co-Authored-By:` trailer
for Claude. Pass the message via a HEREDOC.
7. Push with `git push`; if the branch has no upstream, `git push -u origin HEAD`.
8. If the branch isn't `main`, remind the user to open an MR with `tea pr create` rather than
opening one automatically.
9. Report the commit hash and push result.
If there are no changes, stop and say so; do not create an empty commit.
+14
View File
@@ -0,0 +1,14 @@
{
"permissions": {
"allow": [
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git branch:*)",
"Bash(git add:*)",
"Bash(date:*)",
"Bash(tea pr create:*)",
"Read(//private/tmp/**)"
]
}
}