Housekeeping: review docs into docs/, refreshed agent guide and settings

Four unrelated working-tree changes, none of them touching operator
behaviour:

- review/*.md -> docs/reviews/2026-09-17-kvm2-provider-review/, byte for
  byte, so the kvm2 review sits with the rest of the docs under a name
  that says which review it was.
- AGENTS.md: replaced the generic kubebuilder scaffold guide with what is
  actually true of this repo — the real component layout, the lint/test
  commands that work here, and the behavioural gotchas (replace-on-change
  hashing, the cloud-init Secret label, in-memory leases).
- .claude/settings.json: the allow list still carried /Users/jan.novak
  paths from the macOS checkout; re-pointed at /home/novakj and collapsed
  the one-off entries into prefixes.
- config/samples/proxy_kvm2_crawl_only_one.yaml: a one-VM cut of the crawl
  pool, the rest of the members left commented out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-18 08:12:27 +00:00
co-authored by Claude Opus 5
parent 1fa3e55b9c
commit 8fa9e8aad9
8 changed files with 319 additions and 422 deletions
+174 -102
View File
@@ -1,113 +1,185 @@
{ {
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(go version *)",
"Bash(go mod *)",
"Read(//Users/jan.novak/.claude/skills/go-project-bootstrap/references/**)",
"Bash(ls -la /Users/jan.novak/.claude/skills/go-project-bootstrap/assets/ /Users/jan.novak/.claude/skills/go-project-bootstrap/assets/commands/ /Users/jan.novak/.claude/skills/go-project-bootstrap/assets/agents/ 2>&1)",
"Read(//Users/jan.novak/.claude/skills/go-project-bootstrap/assets/**)",
"Read(//Users/jan.novak/.claude/skills/go-project-bootstrap/assets/agents/**)",
"Read(//Users/jan.novak/.claude/skills/go-project-bootstrap/assets/commands/**)",
"Bash(mkdir -p /Users/jan.novak/srv/go/egress-proxies-operator/docs/plans && touch /Users/jan.novak/srv/go/egress-proxies-operator/docs/plans/.gitkeep && mkdir -p /Users/jan.novak/srv/go/egress-proxies-operator/.claude/commands /Users/jan.novak/srv/go/egress-proxies-operator/.claude/agents && ls -la /Users/jan.novak/srv/go/egress-proxies-operator)",
"Bash(git status)",
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git branch:*)",
"Bash(git add:*)",
"Bash(git checkout:*)",
"Bash(git pull:*)",
"Bash(git fetch:*)",
"Bash(date:*)",
"Read(//tmp/**)",
"Read(//private/tmp/**)",
"Bash(go build:*)",
"Bash(go test:*)",
"Bash(go vet:*)",
"Bash(go mod:*)",
"Bash(go run:*)",
"Bash(go version:*)",
"Bash(go get:*)",
"Bash(golangci-lint run:*)",
"Bash(golangci-lint --version)",
"Bash(gofumpt:*)",
"Bash(tea pr create:*)",
"Bash(curl -sL https://go.dev/dl/?mode=json)",
"Bash(find / -iname \"docs-conventions.md\" -maxdepth 6)",
"Read(//Users/jan.novak/.claude/**)",
"Read(//Users/jan.novak/.claude/skills/**)",
"Bash(mkdir -p docs/plans && date \"+%Y-%m-%d-%H%M\")",
"Bash(go install *)",
"Bash(go env *)",
"Bash(git rm *)",
"Bash(mkdir -p /Users/jan.novak/srv/go/egress-proxies-operator/docs/plans-executions)",
"Bash(cd /Users/jan.novak/srv/go/egress-proxies-operator *)",
"Bash(echo \"build: $?\")",
"Bash(echo \"vet: $?\")",
"Bash(perl -i -pe 's{^\\\\t\\\\t\\\\t\\\\t\\\\t// TODO\\\\\\(user\\\\\\): Specify other spec details if needed\\\\.\\\\n}{\\\\t\\\\t\\\\t\\\\t\\\\t// A minimal, schema-valid spec so this placeholder test survives the\\\\n\\\\t\\\\t\\\\t\\\\t\\\\t// CEL/CRD validation added in Step 1. Rewritten wholesale in Step 4\\\\n\\\\t\\\\t\\\\t\\\\t\\\\t// alongside the real reconciler and envtest suite.\\\\n\\\\t\\\\t\\\\t\\\\t\\\\tSpec: crawlv1alpha1.ProxySpec{\\\\n\\\\t\\\\t\\\\t\\\\t\\\\t\\\\tMode: crawlv1alpha1.ModeExternal,\\\\n\\\\t\\\\t\\\\t\\\\t\\\\t\\\\tEndpoint: &crawlv1alpha1.EndpointSpec{Host: \"10.0.0.1\"},\\\\n\\\\t\\\\t\\\\t\\\\t\\\\t},\\\\n}' internal/controller/proxy_controller_test.go)",
"Bash(go tool *)",
"Bash(docker version *)",
"Bash(curl -sI --max-time 5 https://hub.docker.com)",
"Bash(kind get *)",
"Bash(curl -s --max-time 5 \"https://hub.docker.com/v2/repositories/ubuntu/squid/tags?page_size=10\")",
"Bash(python3 -c ' *)",
"Bash(curl -s --max-time 5 \"https://hub.docker.com/v2/repositories/ubuntu/squid/tags?page_size=100\")",
"Bash(go doc *)",
"Bash(go list *)",
"Bash(gofmt -w internal/provider/config.go)",
"Bash(gofmt -l .)",
"Bash(git restore *)",
"Bash(make manifests *)",
"Bash(make test *)",
"Bash(KUBEBUILDER_ASSETS=\"/Users/jan.novak/srv/go/egress-proxies-operator/bin/k8s/1.36.2-darwin-arm64\" go test -race ./internal/controller/)",
"Bash(grep -n 'func Channel' -A8 __CMDSUB_OUTPUT__/sigs.k8s.io/controller-runtime@v0.24.1/pkg/source/source.go)",
"Bash(grep -n 'type GenericEvent' __CMDSUB_OUTPUT__/sigs.k8s.io/controller-runtime@v0.24.1/pkg/event/event.go)",
"Bash(KUBEBUILDER_ASSETS=__TRACKED_VAR__/bin/k8s/1.36.2-darwin-arm64 go test -race ./...)",
"Bash(cat >> *)",
"Bash(make run-dev *)",
"Bash(kubectl get *)",
"Bash(kubectl delete *)",
"Bash(make docker-build *)",
"Bash(kind load *)",
"Bash(make deploy *)",
"Bash(kubectl -n egress-proxies-operator-system rollout status deploy/egress-proxies-operator-controller-manager --timeout=120s)",
"Bash(kubectl -n egress-proxies-operator-system rollout restart deploy/egress-proxies-operator-controller-manager)",
"Bash(git -C /Users/jan.novak/srv/go/egress-proxies-operator add docs/plans/2026-08-11-1742-gcp-provider-verbose-logging.md)",
"Bash(git -C /Users/jan.novak/srv/go/egress-proxies-operator commit -m 'Add plan: verbose V-level logging in the GCP provider *)",
"Bash(echo \"exit: $?\")",
"Bash(echo \"tests exit: $?\")",
"Bash(./bin/manager --version)",
"Bash(./bin/manager-stamped --version)",
"Bash(./bin/manager-pkg --version)",
"Bash(docker run *)",
"Bash(kubectl -n egress-proxies-operator-system get pods -o wide)",
"Bash(kubectl -n egress-proxies-operator-system get deploy egress-proxies-operator-controller-manager -o jsonpath='{.spec.template.spec.containers[0].args}')",
"Bash(kubectl -n egress-proxies-operator-system logs deploy/egress-proxies-operator-controller-manager)",
"Bash(python3 -c \"import json; d=json.load\\(open\\('docs/deploy/sa_key.json'\\)\\); print\\(d.get\\('type'\\), d.get\\('client_email'\\)\\)\")",
"Bash(tea pr *)",
"Bash(git worktree *)",
"Bash(python3 -c \"import yaml; yaml.safe_load\\(open\\('.gitea/workflows/build.yaml'\\)\\); print\\('YAML OK'\\)\")",
"Bash(ruby -ryaml -e \"YAML.load_file\\('.gitea/workflows/build.yaml'\\); puts 'YAML OK'\")",
"Bash(git -C /Users/jan.novak/srv/go/egress-proxies-operator status --short --branch)",
"Bash(git -C /Users/jan.novak/srv/go/egress-proxies-operator log --oneline -1)",
"Bash(git -C /Users/jan.novak/srv/go/egress-proxies-operator tag 0.01)",
"Bash(git -C /Users/jan.novak/srv/go/egress-proxies-operator push origin 0.01)",
"Bash(chmod +x docs/demo/run-demo.sh)",
"Bash(bash -n docs/demo/run-demo.sh)",
"Bash(git add *)",
"Bash(git commit -q -m 'Add plan: KVM/libvirt provider *)",
"Bash(go get *)",
"Bash(go build *)", "Bash(go build *)",
"Bash(go test *)", "Bash(go test *)",
"Bash(git commit *)" "Bash(go vet *)",
"Bash(go run *)",
"Bash(go mod *)",
"Bash(go get *)",
"Bash(go install *)",
"Bash(go env *)",
"Bash(go tool *)",
"Bash(go doc *)",
"Bash(go list *)",
"Bash(go version *)",
"Bash(gofmt *)",
"Bash(gofumpt *)",
"Bash(golangci-lint run *)",
"Bash(golangci-lint --version)",
"Bash(./bin/manager* --version)",
"Bash(make -n *)",
"Bash(make manifests *)",
"Bash(make test *)",
"Bash(make test-integration-kvm *)",
"Bash(make run-dev *)",
"Bash(make docker-build *)",
"Bash(make deploy *)",
"Bash(make kvm-ssh-secret *)",
"Bash(git status *)",
"Bash(git diff *)",
"Bash(git log *)",
"Bash(git branch)",
"Bash(git branch -a)",
"Bash(git branch --show-current)",
"Bash(git add *)",
"Bash(git commit *)",
"Bash(git fetch *)",
"Bash(git pull *)",
"Bash(git worktree list *)",
"Bash(git config --get *)",
"Bash(git config --list *)",
"Bash(git merge-tree *)",
"Bash(git ls-remote *)",
"Bash(git -c credential.helper='!f\\(\\) { echo username=kacerr; echo \"password=$\\(tr -d \"[:space:]\" < ~/.config/gitea-token\\)\"; }; f' push origin feat/kvm-provider)",
"Bash(git -c credential.helper='!f\\(\\) { echo username=kacerr; echo \"password=$\\(tr -d \"[:space:]\" < ~/.config/gitea-token\\)\"; }; f' push -q origin feat/kvm-provider)",
"Bash(git -c credential.helper='!f\\(\\) { echo username=kacerr; echo \"password=$\\(tr -d \"[:space:]\" < ~/.config/gitea-token\\)\"; }; f' push -u origin feat/kvm-provider)",
"Bash(tea pr create *)",
"Bash(tea pr list *)",
"Bash(curl -sL https://go.dev/dl/?mode=json)",
"Bash(curl -s --max-time 10 https://gitea.home.hrajfrisbee.cz/api/v1/repos/kacerr/egress-proxies-operator/*)",
"Bash(curl -s --max-time 10 'https://gitea.home.hrajfrisbee.cz/api/v1/repos/kacerr/egress-proxies-operator/*')",
"Bash(curl -sI --max-time 10 https://gitea.home.hrajfrisbee.cz/v2/kacerr/egress-proxies-operator/*)",
"Bash(curl -s -H \"Authorization: token $\\(tr -d '[:space:]' < ~/.config/gitea-token\\)\" https://gitea.home.hrajfrisbee.cz/api/v1/repos/kacerr/egress-proxies-operator/pulls/5)",
"Bash(kubectl get *)",
"Bash(kubectl wait *)",
"Bash(kubectl kustomize *)",
"Bash(kubectl apply *)",
"Bash(kubectl run *)",
"Bash(kubectl config current-context)",
"Bash(kubectl config get-contexts *)",
"Bash(kubectl config view *)",
"Bash(kubectl -n egress-proxies-operator-system get *)",
"Bash(kubectl -n egress-proxies-operator-system logs *)",
"Bash(kubectl -n egress-proxies-operator-system rollout status *)",
"Bash(kubectl -n egress-proxies-operator-system rollout restart *)",
"Bash(kind get *)",
"Bash(kind load *)",
"Bash(kind version *)",
"Bash(docker version *)",
"Bash(docker info *)",
"Bash(docker image ls *)",
"Bash(docker image inspect *)",
"Bash(docker manifest inspect *)",
"Bash(docker history *)",
"Bash(docker save *)",
"Bash(docker run *)",
"Bash(docker exec *)",
"Bash(virsh -c qemu:///system list *)",
"Bash(virsh -c qemu:///system net-list *)",
"Bash(virsh -c qemu:///system net-dumpxml *)",
"Bash(virsh -c qemu:///system pool-list *)",
"Bash(virsh -c qemu:///system pool-dumpxml *)",
"Bash(virsh -c qemu:///system vol-list *)",
"Bash(ssh -i ~/.ssh/proxy-operator_ed25519 * novakj@beelink.tail8c69c.ts.net *)",
"Bash(ip -br *)",
"Bash(getent hosts *)",
"Bash(tailscale status *)",
"Bash(python3 *)",
"Bash(grep *)",
"Bash(echo \"*$?\")",
"Bash(date *)",
"Bash(env)",
"Bash(command -v *)",
"Bash(cat >> *)",
"Bash(tar -tf *)",
"Bash(bash -n *)",
"Bash(mkdir -p docs/*)",
"Read(//home/novakj/**)",
"Read(//Users/jan.novak/.claude/**)",
"Read(//tmp/**)",
"Read(//private/tmp/**)",
"Read(//etc/**)",
"Read(//dev/**)",
"Read(//run/libvirt/**)",
"WebSearch",
"WebFetch(domain:gitlab.com)",
"WebFetch(domain:bugs.debian.org)",
"WebFetch(domain:tracker.debian.org)",
"WebFetch(domain:cloud.debian.org)",
"WebFetch(domain:alioth-lists.debian.net)",
"WebFetch(domain:www.mail-archive.com)",
"WebFetch(domain:forum.proxmox.com)",
"Bash(curl -s --max-time 10 'https://gitea.home.hrajfrisbee.cz/api/v1/repos/kacerr/egress-proxies-operator/pulls?state=open')",
"Bash(python3 -c 'import json,sys; [print\\(p[\"number\"], p[\"head\"][\"ref\"], \"->\", p[\"base\"][\"ref\"], \"|\", p[\"title\"]\\) for p in json.load\\(sys.stdin\\)]')",
"Bash(curl -s --max-time 10 'https://gitea.home.hrajfrisbee.cz/api/v1/repos/kacerr/egress-proxies-operator/pulls/5')",
"Bash(python3 -c 'import json,sys; p=json.load\\(sys.stdin\\); print\\(p[\"state\"], \"merged=\", p[\"merged\"], p.get\\(\"merge_commit_sha\"\\)\\)')",
"Read(//home/novakj/srv/egress-proxies-operator-simplify/origin/**)",
"Bash(echo \"exit=$?\")",
"Read(//home/novakj/srv/egress-proxies-operator-simplify/**)",
"Bash(git -C /home/novakj/srv/egress-proxies-operator-simplify diff 82eeeef -- internal/controller/proxy_controller.go internal/provider/kvm/errors.go)",
"Bash(git switch *)",
"Bash(git -C /home/novakj/srv/egress-proxies-operator-simplify add -A)",
"Bash(git -C /home/novakj/srv/egress-proxies-operator-simplify commit -q -m 'WIP: uncommitted simplify-pass edits preserved before worktree removal *)",
"Bash(git -C /home/novakj/srv/egress-proxies-operator-simplify log --oneline -1)",
"Bash(git worktree *)",
"Bash(git branch *)",
"Bash(git stash *)",
"Bash(make generate *)",
"Bash(bin/kustomize build *)",
"Bash(grep -Ev \"^\\\\s*$\")",
"Bash(make lint *)",
"Bash(command -v kind docker)",
"Bash(bin/golangci-lint version *)",
"Bash(make test-e2e *)",
"Bash(echo \"make test-e2e exit=$?\")",
"Bash(ls \"$\\(go env GOMODCACHE\\)/golang.org/\")",
"Bash(ls \"$\\(go env GOMODCACHE\\)/golang.org/toolchain@\"* -d)",
"Bash(ps -eo pid,ppid,etime,args)",
"Bash(kubectl config *)",
"Bash(git commit -q -m 'Plan the provider blueprint document *)",
"Bash(grep -nE '^#{1,3} ' docs/architecture.md; echo ---; sed -n '/Implemented providers/,/^## /p' README.md | head -30; echo ---; sed -n 140,180p config/manager/manager.yaml; echo ---; cat Dockerfile; echo ---; ls -a | grep -i golangci; echo ---; grep -nE 'Placement|placement|Region|Zone|MachineType|Image ' api/v1alpha1/proxy_types.go | head -40; echo ---; grep -nE 'Type|type' api/v1alpha1/providerhealth_types.go | head -20; echo ---; grep -n -i 'placement' docs/api.md | head; echo ---; grep -nE 'WithTimeout|Deadline|timeout' internal/controller/proxy_controller.go internal/gc/gc.go internal/providerhealth/monitor.go | head -20; echo ---; cat .gitea/workflows/build.yaml | sed -n 1,60p)",
"WebFetch(domain:pve.proxmox.com)",
"Bash(git -c credential.helper='!f\\(\\) { echo username=kacerr; echo \"password=$\\(tr -d \"[:space:]\" < ~/.config/gitea-token\\)\"; }; f' push -u origin feat/provider-blueprint)",
"Bash(curl -s -H \"Authorization: token $\\(tr -d '[:space:]' < ~/.config/gitea-token\\)\" \"https://gitea.home.hrajfrisbee.cz/api/v1/repos/kacerr/egress-proxies-operator/pulls?state=open\")",
"Bash(curl -s -H \"Authorization: token $\\(tr -d '[:space:]' < ~/.config/gitea-token\\)\" \"https://gitea.home.hrajfrisbee.cz/api/v1/repos/kacerr/egress-proxies-operator/pulls/7\")",
"Bash(curl -s -X PATCH -H \"Authorization: token $\\(tr -d '[:space:]' < ~/.config/gitea-token\\)\" -H 'Content-Type: application/json' -d @/tmp/claude-1000/-home-novakj-srv-egress-proxies-operator/7662ab9d-4540-4676-a943-0874ac6b081d/scratchpad/pr7.json \"https://gitea.home.hrajfrisbee.cz/api/v1/repos/kacerr/egress-proxies-operator/pulls/7\")",
"Bash(bin/golangci-lint run *)",
"Bash(awk '/^## The image cache/,/^## [^T]/' docs/kvm-provider.md)",
"Bash(awk '/^#+ .*Step 11/,0' docs/plans/2026-09-16-1333-kvm2-provider.md)",
"Bash(sort -t: -k2 -u)",
"Bash(awk '/kvm2/&&/^#/{f=1} f' docs/testing.md)",
"Bash(awk -F'\"' '{print $2}')",
"Bash(sed -n '/targets:/,/type: object/p' config/crd/bases/*providerhealths.yaml | sed -n 1,45p)",
"Bash(ssh -i ~/.ssh/proxy-operator_ed25519 -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=10 -o UserKnownHostsFile=~/.ssh/kvm_known_hosts -o StrictHostKeyChecking=yes novakj@beelink 'hostname; id -un; virsh -c qemu:///system version; virsh -c qemu:///system pool-list --all; virsh -c qemu:///system list --all')",
"Bash(awk '{print $1, $2}' ~/.ssh/kvm_known_hosts)",
"Bash(ssh-keygen -F beelink -f ~/.ssh/kvm_known_hosts)",
"Bash(ssh -i ~/.ssh/proxy-operator_ed25519 -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=10 -o UserKnownHostsFile=~/.ssh/kvm_known_hosts -o StrictHostKeyChecking=yes novakj@beelink.tail8c69c.ts.net 'hostname; id -un; id -Gn; virsh -c qemu:///system version; virsh -c qemu:///system pool-list --all --details; virsh -c qemu:///system list --all; command -v qemu-img')",
"Bash(qemu-img --version)",
"Bash(./probe)",
"Bash(./probe -proxyrcv 8192)",
"Bash(./probe -small)",
"Bash(sysctl net.ipv4.tcp_wmem net.ipv4.tcp_retries2)",
"Bash(GOFLAGS=-mod=mod GOPROXY=off go build -o probe .)",
"Bash(./probe -clientsnd 131072 -proxyrcv 8192)",
"Bash(truncate -s 64M gpt.raw)",
"Bash(sfdisk -l gptshort.raw)",
"Bash(sgdisk -v gptshort.raw)",
"Bash(mkdir -p /tmp/claude-1000/-home-novakj-srv-egress-proxies-operator/8db56daf-938c-48b8-8a17-084f3e1dea44/scratchpad)",
"Bash(curl -sS -m 30 -o storage_driver.c https://raw.githubusercontent.com/libvirt/libvirt/v11.6.0/src/storage/storage_driver.c)",
"Bash(GOFLAGS=-mod=mod GOPROXY=off go vet ./...)",
"Bash(GOFLAGS=-mod=mod GOPROXY=off go test -count=1 -run TestLeak -v . -args -n 1000 -transport unix)",
"Bash(curl -sS -m 30 -o storage_util.c https://raw.githubusercontent.com/libvirt/libvirt/v11.6.0/src/storage/storage_util.c)",
"Bash(awk '/^virStorageBackendVolUploadLocal/,/^}/' storage_util.c)",
"Bash(export GOFLAGS=-mod=mod GOPROXY=off)",
"Bash(awk '/WARNING: DATA RACE/{c++} c==1' race_ssh.log)",
"Bash(kubectl version *)",
"Bash(timeout 5 kubectl cluster-info)"
], ],
"additionalDirectories": [ "additionalDirectories": [
"/Users/jan.novak/srv/go/egress-proxies-operator/.claude",
"/Users/jan.novak/srv/go/egress-proxies-operator/docs/plans",
"/Users/jan.novak/srv/go/egress-proxies-operator/docs",
"/Users/jan.novak/srv/go/egress-proxies-operator/docs/prompts",
"/tmp", "/tmp",
"/Users/jan.novak/srv/go/egress-proxies-operator/docs/plans-executions" "/home/novakj/srv/egress-proxies-operator",
"/home/novakj/srv/egress-proxies-operator/config/crd/bases"
] ]
} }
} }
+42 -320
View File
@@ -1,320 +1,42 @@
# egress-proxies-operator - AI Agent Guide # egress-proxies-operator
## Project Structure ## Sources and architecture
**Single-group layout (default):** - Trust `go.mod`, `Makefile`, and current code over version tables or bootstrap prose. `CLAUDE.md`'s opening architecture/commands sections are stale; this is a working operator, not an empty scaffold.
``` - `cmd/main.go` assembles the provider registry, reconciler, proxy health engine, in-memory lease store, discovery HTTP API, orphan GC, and provider-health monitor on one controller-runtime manager.
cmd/main.go Manager entry (registers controllers/webhooks) - `api/v1alpha1` contains namespaced `Proxy` and cluster-scoped `ProviderHealth` resources (`crawl.example.com`). Validation/defaulting uses CRD markers/CEL; there are no admission webhooks.
api/<version>/*_types.go CRD schemas (+kubebuilder markers) - `internal/provider` defines the backend contract independently of CRD types. Constructors are registered in `cmd/main.go`; `spec.provider` selects a configured **instance name**, not a provider type. `kvm` and `kvm2` are separate implementations and do not adopt each other's VMs.
api/<version>/zz_generated.* Auto-generated (DO NOT EDIT) - Before changing a provider, read `docs/provider-contract.md`; use `docs/provider-spec-template.md` for a new one. Callers rely on self-contained provider IDs, truthful `ErrNotFound`, idempotent create/delete, and persisted UID/creation time for orphan GC. See `docs/providers-existing.md` for implementation-specific exceptions.
internal/controller/* Reconciliation logic - `docs/architecture.md` explains component ownership and state transitions; `docs/api.md` documents discovery/leases; `docs/testing.md` covers test boundaries and real-infrastructure setup.
internal/webhook/* Validation/defaulting (if present)
config/crd/bases/* Generated CRDs (DO NOT EDIT) ## Commands and verification
config/rbac/role.yaml Generated RBAC (DO NOT EDIT)
config/samples/* Example CRs (edit these) - Go version comes from `go.mod` (currently 1.26.0). Make installs development tools into `bin/`.
Makefile Build/test/deploy commands - After Go changes: `make lint-fix`, then `make test`. The latter regenerates manifests/DeepCopy, formats, vets, sets up envtest, then runs non-e2e tests with `-race` and writes `cover.out`.
PROJECT Kubebuilder metadata Auto-generated (DO NOT EDIT) - Fast checks: `go test -short -race ./...` skips envtest. Focused package/test: `go test -race ./internal/lease/` or `go test -race -run TestAcquire ./internal/lease/`.
``` - Envtest needs local API-server/etcd binaries; use `make setup-envtest` before targeted runs. For the controller suite with the current Kubernetes minor:
```sh
**Multi-group layout** (for projects with multiple API groups): KUBEBUILDER_ASSETS="$(./bin/setup-envtest use 1.36 --bin-dir "$PWD/bin" -p path)" go test -race ./internal/controller/ -run TestControllers -ginkgo.focus='<spec regex>'
``` ```
api/<group>/<version>/*_types.go CRD schemas by group The Makefile derives the envtest **minor** from `go.mod`; do not replace it with a dependency patch version that may have no published binary bundle.
internal/controller/<group>/* Controllers by group - Tests are mostly stdlib `testing`; controller envtest and e2e use Ginkgo/Gomega. Fake clients do not execute CEL or schema defaults: test those with envtest, and use the API's `*OrDefault` helpers in unit tests. Envtest has no kubelet, so it cannot prove Squid starts or serves traffic.
internal/webhook/<group>/<version>/* Webhooks by group and version (if present) - `make lint` / `make lint-fix` builds a custom golangci-lint with the `logcheck` plugin from `.custom-gcl.yml`; a stock global binary is insufficient. `.golangci.yml` forbids `sort` (use `slices`). Logging uses controller-runtime/logr, with Kubernetes-style messages.
``` - Real-host tests are opt-in: `make test-integration-kvm` / `make test-integration-kvm2` require the corresponding `KVM_TEST_*` / `KVM2_TEST_*` variables from `docs/testing.md`. After provider refactors, compile-check these with `go vet -tags=integration ./...`.
- E2E requires Docker, kind, kubectl, and an isolated cluster. `make test-e2e` uses `KIND_CLUSTER=egress-proxies-operator-test-e2e`; ensure kubectl targets that cluster, especially if it already exists. On failure, run `make cleanup-test-e2e` explicitly. Compile-check with `go vet -tags=e2e ./...`. Tracing specs additionally require both `TEMPO_URL` and `OTLP_ENDPOINT`.
Multi-group layout organizes APIs by group name (e.g., `batch`, `apps`). Check the `PROJECT` file for `multigroup: true`. - `.gitea/workflows/build.yaml` runs only on tags/manual dispatch; its gate is `go vet ./...`, `go build ./...`, `go test -short ./...`. It does not replace local lint, race, or envtest verification.
**To convert to multi-group layout:** ## Code generation
1. Run: `kubebuilder edit --multigroup=true`
2. Move APIs: `mkdir -p api/<group> && mv api/<version> api/<group>/` - After API types or Kubebuilder markers change, run `make manifests generate`. Edit source markers, never generated `config/crd/bases/*.yaml`, `config/rbac/role.yaml`, or `**/zz_generated.*.go`.
3. Move controllers: `mkdir -p internal/controller/<group> && mv internal/controller/*.go internal/controller/<group>/` - Keep `+kubebuilder:rbac` markers in standalone package-level comment blocks, outside function/type documentation; otherwise controller-gen can silently ignore them. Inspect `git diff config/rbac/role.yaml` after generation.
4. Move webhooks (if present): `mkdir -p internal/webhook/<group> && mv internal/webhook/<version> internal/webhook/<group>/` - Preserve `+kubebuilder:scaffold:*` markers and the single-group layout. Use `kubebuilder create api` / `kubebuilder create webhook` for new scaffolding; `PROJECT` is CLI-managed.
5. Update import paths in all files
6. Fix `path` in `PROJECT` file for each resource ## Behaviour and operational gotchas
7. Update test suite CRD paths (add one more `..` to relative paths)
- Managed proxies are replace-on-change: `internal/controller/spechash.go` hashes placement, resolved cloud-init content, and effective port. Do not hash all of `ProxySpec`: attributes, lease limits, and health settings must not churn VMs.
## Critical Rules - Cloud-init Secrets must carry `crawl.example.com/cloud-init: "true"`; the manager's Secret cache excludes unlabelled Secrets. Rotating labelled Secret content triggers VM replacement.
- Proxy health checks fetch **through** the proxy. The health engine supplies snapshots/events and the reconciler writes status; status-affecting transitions drive writes, so `lastHealthCheckTime` is not a last-probe heartbeat.
### Never Edit These (Auto-Generated) - Leases/cooldowns are in-memory and disappear on restart. Discovery is not leader-elected; the deployment uses one replica. Adding replicas alone does not provide shared lease capacity.
- `config/crd/bases/*.yaml` - from `make manifests` - Use `make run-dev` locally: plain `make run` omits the required `--providers-config`. Host-side probes generally cannot route to kind pod IPs; deploy in-cluster using the README quickstart to verify kubernetes-provider health.
- `config/rbac/role.yaml` - from `make manifests` - `make deploy IMG=...` and `make build-installer IMG=...` modify `config/manager/kustomization.yaml` in place. Check that diff before including it in a change.
- `config/webhook/manifests.yaml` - from `make manifests` - Namespace-restricted mode disables orphan GC by default. `--gc-allow-namespaced` bypasses that guard and can classify instances owned outside the visible namespace as orphans.
- `**/zz_generated.*.go` - from `make generate`
- `PROJECT` - from `kubebuilder [OPTIONS]`
### Never Remove Scaffold Markers
Do NOT delete `// +kubebuilder:scaffold:*` comments. CLI injects code at these markers.
### Keep Project Structure
Do not move files around. The CLI expects files in specific locations.
### Always Use CLI Commands
Always use `kubebuilder create api` and `kubebuilder create webhook` to scaffold. Do NOT create files manually.
### E2E Tests Require an Isolated Kind Cluster
The e2e tests are designed to validate the solution in an isolated environment (similar to GitHub Actions CI).
Ensure you run them against a dedicated [Kind](https://kind.sigs.k8s.io/) cluster (not your “real” dev/prod cluster).
## After Making Changes
**After editing `*_types.go` or markers:**
```
make manifests # Regenerate CRDs/RBAC from markers
make generate # Regenerate DeepCopy methods
```
**After editing `*.go` files:**
```
make lint-fix # Auto-fix code style
make test # Run unit tests
```
## CLI Commands Cheat Sheet
### Create API (your own types)
```bash
kubebuilder create api --group <group> --version <version> --kind <Kind>
```
### Deploy Image Plugin (scaffold to deploy/manage ANY container image)
Generate a controller that deploys and manages a container image (nginx, redis, memcached, your app, etc.):
```bash
# Example: deploying memcached
kubebuilder create api --group example.com --version v1alpha1 --kind Memcached \
--image=memcached:alpine \
--plugins=deploy-image.go.kubebuilder.io/v1-alpha
```
Scaffolds good-practice code: reconciliation logic, status conditions, finalizers, RBAC. Use as a reference implementation.
### Create Webhooks
```bash
# Validation + defaulting
kubebuilder create webhook --group <group> --version <version> --kind <Kind> \
--defaulting --programmatic-validation
# Conversion webhook (for multi-version APIs)
kubebuilder create webhook --group <group> --version v1 --kind <Kind> \
--conversion --spoke v2
```
### Controller for Core Kubernetes Types
```bash
# Watch Pods
kubebuilder create api --group core --version v1 --kind Pod \
--controller=true --resource=false
# Watch Deployments
kubebuilder create api --group apps --version v1 --kind Deployment \
--controller=true --resource=false
```
### Controller for External Types (e.g., from other operators)
Watch resources from external APIs (cert-manager, Argo CD, Istio, etc.):
```bash
# Example: watching cert-manager Certificate resources
kubebuilder create api \
--group cert-manager --version v1 --kind Certificate \
--controller=true --resource=false \
--external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \
--external-api-domain=io \
--external-api-module=github.com/cert-manager/cert-manager
```
**Note:** Use `--external-api-module=<module>@<version>` only if you need a specific version. Otherwise, omit `@<version>` to use what's in go.mod.
### Webhook for External Types
```bash
# Example: validating external resources
kubebuilder create webhook \
--group cert-manager --version v1 --kind Issuer \
--defaulting \
--external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \
--external-api-domain=io \
--external-api-module=github.com/cert-manager/cert-manager
```
## Testing & Development
```bash
make test # Run unit tests (uses envtest: real K8s API + etcd)
make run # Run locally (uses current kubeconfig context)
```
Tests use **Ginkgo + Gomega** (BDD style). Check `suite_test.go` for setup.
## Deployment Workflow
```bash
# 1. Regenerate manifests
make manifests generate
# 2. Build & deploy
export IMG=<registry>/<project>:tag
make docker-build docker-push IMG=$IMG # Or: kind load docker-image $IMG --name <cluster>
make deploy IMG=$IMG
# 3. Test
kubectl apply -k config/samples/
# 4. Debug
kubectl logs -n <project>-system deployment/<project>-controller-manager -c manager -f
```
### API Design
**Key markers for** `api/<version>/*_types.go`:
```go
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Namespaced
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=".status.conditions[?(@.type=='Ready')].status"
// On fields:
// +kubebuilder:validation:Required
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:MaxLength=100
// +kubebuilder:validation:Pattern="^[a-z]+$"
// +kubebuilder:default="value"
```
- **Use** `metav1.Condition` for status (not custom string fields)
- **Use predefined types**: `metav1.Time` instead of `string` for dates
- **Follow K8s API conventions**: Standard field names (`spec`, `status`, `metadata`)
### Controller Design
**RBAC markers in** `internal/controller/*_controller.go`:
```go
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/finalizers,verbs=update
// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
```
**Implementation rules:**
- **Idempotent reconciliation**: Safe to run multiple times
- **Re-fetch before updates**: `r.Get(ctx, req.NamespacedName, obj)` before `r.Update` to avoid conflicts
- **Structured logging**: `log := log.FromContext(ctx); log.Info("msg", "key", val)`
- **Owner references**: Enable automatic garbage collection (`SetControllerReference`)
- **Watch secondary resources**: Use `.Owns()` or `.Watches()`, not just `RequeueAfter`
- **Finalizers**: Clean up external resources (buckets, VMs, DNS entries)
### Logging
**Follow Kubernetes logging message style guidelines:**
- Start from a capital letter
- Do not end the message with a period
- Active voice: subject present (`"Deployment could not create Pod"`) or omitted (`"Could not create Pod"`)
- Past tense: `"Could not delete Pod"` not `"Cannot delete Pod"`
- Specify object type: `"Deleted Pod"` not `"Deleted"`
- Balanced key-value pairs
```go
log.Info("Starting reconciliation")
log.Info("Created Deployment", "name", deploy.Name)
log.Error(err, "Failed to create Pod", "name", name)
```
**Reference:** https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines
### Webhooks
- **Create all types together**: `--defaulting --programmatic-validation --conversion`
- **When`--force`is used**: Backup custom logic first, then restore after scaffolding
- **For multi-version APIs**: Use hub-and-spoke pattern (`--conversion --spoke v2`)
- Hub version: Usually oldest stable version (v1)
- Spoke versions: Newer versions that convert to/from hub (v2, v3)
- Example: `--group crew --version v1 --kind Captain --conversion --spoke v2` (v1 is hub, v2 is spoke)
### Learning from Examples
The **deploy-image plugin** scaffolds a complete controller following good practices. Use it as a reference implementation:
```bash
kubebuilder create api --group example --version v1alpha1 --kind MyApp \
--image=<your-image> --plugins=deploy-image.go.kubebuilder.io/v1-alpha
```
Generated code includes: status conditions (`metav1.Condition`), finalizers, owner references, events, idempotent reconciliation.
## Distribution Options
### Option 1: YAML Bundle (Kustomize)
```bash
# Generate dist/install.yaml from Kustomize manifests
make build-installer IMG=<registry>/<project>:tag
```
**Key points:**
- The `dist/install.yaml` is generated from Kustomize manifests (CRDs, RBAC, Deployment)
- Commit this file to your repository for easy distribution
- Users only need `kubectl` to install (no additional tools required)
**Example:** Users install with a single command:
```bash
kubectl apply -f https://raw.githubusercontent.com/<org>/<repo>/<tag>/dist/install.yaml
```
### Option 2: Helm Chart
```bash
kubebuilder edit --plugins=helm/v2-alpha # Generates dist/chart/ (default)
kubebuilder edit --plugins=helm/v2-alpha --output-dir=charts # Generates charts/chart/
```
**For development:**
```bash
make helm-deploy IMG=<registry>/<project>:<tag> # Deploy manager via Helm
make helm-deploy IMG=$IMG HELM_EXTRA_ARGS="--set ..." # Deploy with custom values
make helm-status # Show release status
make helm-uninstall # Remove release
make helm-history # View release history
make helm-rollback # Rollback to previous version
```
**For end users/production:**
```bash
helm install my-release ./<output-dir>/chart/ --namespace <ns> --create-namespace
```
**Important:** If you add webhooks or modify manifests after initial chart generation:
1. Backup any customizations in `<output-dir>/chart/values.yaml` and `<output-dir>/chart/manager/manager.yaml`
2. Re-run: `kubebuilder edit --plugins=helm/v2-alpha --force` (use same `--output-dir` if customized)
3. Manually restore your custom values from the backup
### Publish Container Image
```bash
export IMG=<registry>/<project>:<version>
make docker-build docker-push IMG=$IMG
```
## References
### Essential Reading
- **Kubebuilder Book**: https://book.kubebuilder.io (comprehensive guide)
- **controller-runtime FAQ**: https://github.com/kubernetes-sigs/controller-runtime/blob/main/FAQ.md (common patterns and questions)
- **Good Practices**: https://book.kubebuilder.io/reference/good-practices.html (why reconciliation is idempotent, status conditions, etc.)
- **Logging Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines (message style, verbosity levels)
### API Design & Implementation
- **API Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md
- **Operator Pattern**: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
- **Markers Reference**: https://book.kubebuilder.io/reference/markers.html
### Tools & Libraries
- **controller-runtime**: https://github.com/kubernetes-sigs/controller-runtime
- **controller-tools**: https://github.com/kubernetes-sigs/controller-tools
- **Kubebuilder Repo**: https://github.com/kubernetes-sigs/kubebuilder
@@ -0,0 +1,103 @@
# A pool of Managed crawl proxies on the kvm2 provider — the multi-proxy
# counterpart to the single proxy_kvm2.yaml sample. Everything here boots on
# the one host the kvm2 entry configures (beelink); the proxies differ in the
# attributes crawlers select on, in size, and in image.
#
# Requires the same prerequisites as proxy_kvm2.yaml: a providers-config
# entry named "kvm2-home" and the kvm-ssh Secret that the kvm and kvm2
# providers share (docs/kvm2-provider.md).
#
# Deliberately NOT in kustomization.yaml: `kubectl apply -k config/samples`
# would boot every VM in here at once. Apply it on its own when you want the
# pool:
#
# kubectl apply -f config/samples/proxy_kvm2_crawl_pool.yaml
#
# Crawlers reach these through the discovery API by attribute, e.g.
# `GET /v1/proxies?attr.purpose=crawl&healthy=true`, or by leasing one with
# the same selector. Attribute matching is exact equality, so keep the keys
# consistent across the pool and vary only the values you want to select on.
#
# The first Proxy for an image on the host waits for that image to download;
# the rest reuse the cached copy. debian-13 and ubuntu-24.04 are separate
# downloads, so the ubuntu member below is the slow one on a cold host.
---
# One cloud-init Secret shared by the whole pool: the proxies below point at
# it with secretRef instead of each carrying its own copy of the squid
# config. The crawl.example.com/cloud-init label is mandatory — the manager's
# cache only holds Secrets that carry it, so an unlabelled Secret is
# invisible to the operator and the Proxy never resolves.
#
# Editing this Secret changes the resolved cloud-init of every Proxy that
# references it, and that replaces each of their VMs. That is the trade for
# sharing one definition: convenient for a config rollout, disruptive if you
# only meant to touch one member. Give a member its own Secret (or an inline
# block) when it needs to diverge.
apiVersion: v1
kind: Secret
metadata:
name: crawl-squid-cloud-init
labels:
crawl.example.com/cloud-init: "true"
type: Opaque
stringData:
# The default key name; SecretKeySelector.key defaults to user-data, so
# the proxies below omit it.
user-data: |
#cloud-config
packages:
- squid
write_files:
- path: /etc/squid/conf.d/proxy-operator.conf
content: |
http_port 3128
http_access allow all
via off
forwarded_for off
runcmd:
- systemctl restart squid
# Optional: log in with `ssh debian@<ip>` (or `ubuntu@<ip>` on the
# ubuntu-24.04 member) to look around; `virsh console <name>` on the
# host works too.
# ssh_authorized_keys:
# - ssh-ed25519 AAAA... you@laptop
---
apiVersion: crawl.example.com/v1alpha1
kind: Proxy
metadata:
name: crawl-home-1
spec:
mode: Managed
provider: kvm2-home
placement:
zone: beelink
machineType: small
image: debian-13
port: 3128
cloudInit:
secretRef:
name: crawl-squid-cloud-init
attributes:
geo: home
purpose: crawl
tier: general
# ---
# apiVersion: crawl.example.com/v1alpha1
# kind: Proxy
# metadata:
# name: crawl-home-2
# spec:
# mode: Managed
# provider: kvm2-home
# placement:
# zone: beelink
# machineType: small
# image: debian-13
# port: 3128
# cloudInit:
# secretRef:
# name: crawl-squid-cloud-init
# attributes:
# geo: home
# purpose: crawl
# tier: general