629 lines
27 KiB
Markdown
629 lines
27 KiB
Markdown
# operator - AI Agent Guide
|
|
|
|
## Smoke Test Suite
|
|
|
|
The smoke suite under `test/e2e/` validates the operator end-to-end against
|
|
a real Kubernetes cluster. It is all-Go, built on Ginkgo/Gomega, and gated
|
|
by Go build tags so unit tests run without it.
|
|
|
|
### Naming convention
|
|
|
|
Targets follow a consistent `-kind` / `-cluster` suffix:
|
|
|
|
| Suffix | What it means |
|
|
|---|---|
|
|
| `-kind` | Self-contained: the target creates a fresh Kind cluster, runs the suite, and deletes the cluster on exit. |
|
|
| `-cluster` | Uses whatever cluster the current `KUBECONFIG` / context points at (OpenShift, EKS, k3s, an existing Kind cluster, …). Requires `IMG=` pointing at a registry the cluster can pull from. |
|
|
|
|
### Targets (M1, no-GPU)
|
|
|
|
```bash
|
|
make test-e2e-kind # local Kind, ~5 min
|
|
make test-e2e-cluster IMG=<registry>/<repo>:<tag> # existing cluster
|
|
```
|
|
|
|
### Targets (M2, GPU)
|
|
|
|
```bash
|
|
make test-e2e-gpu-kind # local Kind, ~30 min
|
|
make test-e2e-gpu-cluster IMG=<registry>/<repo>:<tag> # existing GPU cluster
|
|
```
|
|
|
|
Both run the M1 + M2 specs (M2 = runtime `/conf` round-trip + vLLM
|
|
integration, under the `e2e_gpu` build tag). Pick `test-e2e-gpu-kind`
|
|
when you have GPUs on the dev box and want a self-contained Kind
|
|
cluster; pick `test-e2e-gpu-cluster` when you're targeting an
|
|
existing OpenShift / EKS / GKE GPU cluster. See *GPU tier* below for
|
|
details.
|
|
|
|
#### `test-e2e-kind` — local Kind run
|
|
|
|
Builds the manager image, loads it into a dedicated Kind cluster
|
|
(`operator-test-e2e-<id>` by default), installs CRDs, deploys the controller,
|
|
runs every `//go:build e2e` spec under `test/e2e/`, then tears the
|
|
cluster down. No prereqs beyond Kind + Docker on `$PATH` (plus network
|
|
egress to GitHub).
|
|
|
|
The suite installs **cert-manager** into the cluster before deploying the
|
|
controller — it issues the mutating webhook's serving cert and injects the
|
|
CA bundle (see `config/certmanager`), so the controller-manager Deployment
|
|
never reaches `Available` without it. Install is skipped when the cluster
|
|
already ships cert-manager (e.g. an existing OpenShift/EKS cluster), and
|
|
the suite only uninstalls what it installed. Override the release with
|
|
`CERT_MANAGER_VERSION` (default `v1.16.3`).
|
|
|
|
#### `test-e2e-cluster` — existing cluster (OpenShift, EKS, k3s, …)
|
|
|
|
For when you already have a cluster running and just want the suite to
|
|
install/deploy → test → undeploy against it. Prereqs:
|
|
|
|
1. **Image pushed to a reachable registry.** The cluster nodes must be
|
|
able to `docker pull` `$IMG`. On OpenShift the simplest path is the
|
|
internal registry — see *Pushing to OpenShift's internal registry*
|
|
below.
|
|
2. **`KUBECONFIG` / current-context** points at the target cluster.
|
|
3. Pass the in-cluster pull URL via `IMG=`. The target fails fast if
|
|
`IMG` is missing or still the default `controller:latest`.
|
|
|
|
```bash
|
|
oc config use-context admin # or kubectl config use-context <name>
|
|
export IMG=image-registry.openshift-image-registry.svc:5000/lmcache-operator-system/operator:v0.0.1
|
|
make test-e2e-cluster IMG=$IMG
|
|
```
|
|
|
|
Under the hood this sets `SMOKE_SKIP_IMAGE_LOAD=true` so the suite
|
|
neither rebuilds nor `kind load`s. The cluster is left intact after
|
|
the run.
|
|
|
|
#### Pushing to OpenShift's internal registry
|
|
|
|
```bash
|
|
# One-time per cluster: expose the registry on a default route
|
|
oc patch configs.imageregistry.operator.openshift.io/cluster \
|
|
--type merge -p '{"spec":{"defaultRoute":true}}'
|
|
|
|
# Log Docker into the registry using your oc session
|
|
oc registry login --to=$HOME/.docker/config.json --skip-check
|
|
|
|
# Build, tag, push
|
|
oc create namespace lmcache-operator-system --dry-run=client -o yaml | oc apply -f -
|
|
export OCP_REGISTRY=$(oc get route default-route -n openshift-image-registry -o jsonpath='{.spec.host}')
|
|
make docker-build IMG=controller:latest
|
|
docker tag controller:latest $OCP_REGISTRY/lmcache-operator-system/operator:v0.0.1
|
|
docker push $OCP_REGISTRY/lmcache-operator-system/operator:v0.0.1
|
|
```
|
|
|
|
The push uses the external route; pods inside the cluster pull via the
|
|
in-cluster service name (`image-registry.openshift-image-registry.svc:5000/...`).
|
|
Both names point at the same image; only the hostnames differ.
|
|
|
|
#### OpenShift caveats
|
|
|
|
- **PodSecurity admission**: test namespaces are pre-labeled
|
|
`pod-security.kubernetes.io/enforce=privileged` so the operator's
|
|
DaemonSet (which always sets `hostIPC=true`, and `privileged=true` only
|
|
when `spec.privileged` is enabled) is accepted at admission time.
|
|
`hostIPC=true` alone is rejected by the `baseline`/`restricted` profiles,
|
|
so the label is required regardless of `privileged`. Harmless on clusters
|
|
that don't enforce PodSecurity.
|
|
- **SCC (Security Context Constraints)**: M1 smokes never wait for
|
|
DaemonSet pods to schedule, so SCC isn't a blocker. If you need pods
|
|
to actually run later (M2/M3 GPU tier), grant the LMCache
|
|
ServiceAccount the `privileged` SCC explicitly.
|
|
|
|
### Specs included in M1
|
|
|
|
| Spec file | Coverage |
|
|
|---|---|
|
|
| `crd_smoke_test.go` | harness sanity check + minimal-CR shape + custom-port propagation |
|
|
| `lifecycle_smoke_test.go` | port update propagation, delete + ownerRef GC, invalid `sizeGB` rejection |
|
|
| `field_coverage_smoke_test.go` | ServiceMonitor (auto-skipped if CRD absent), `extraArgs` override, `resourceOverrides` |
|
|
| `auth_smoke_test.go` | cross-namespace `authSecretRef` mirroring + env-var injection |
|
|
|
|
### Specs included in M2 (GPU, build tag `e2e_gpu`)
|
|
|
|
| Spec file | Coverage |
|
|
|---|---|
|
|
| `runtime_smoke_test.go` | HTTP `/conf` round-trip — proves CR field values reach the live LMCache server (not just the K8s objects) by asserting `mp.port` / `mp.chunk_size` / `mp.max_workers` / `mp.hash_algorithm` / `http.http_port` against the running pod's `/conf` payload. |
|
|
| `vllm_integration_smoke_test.go` | vLLM + LMCache round-trip — spins up a vLLM `Deployment` configured against the operator's `<engine>-connection` ConfigMap with `--no-enable-prefix-caching`, sends the same long prompt twice, and asserts `lmcache:num_hit_tokens` on the LMCache `/metrics` endpoint increments on the second call. |
|
|
| `cacheblend_integration_smoke_test.go` | vLLM + CacheBlendEngine round-trip — reconciles a `CacheBlendEngine` (blend server DaemonSet), creates an args-only vLLM `Deployment` that opts into CacheBlend injection (label `lmcache.ai/cacheblend-inject` + engine annotation), and asserts: the mutating webhook stamped `cacheblend-injected=true` and injected `--attention-backend CUSTOM`; vLLM logs the CUSTOM backend banner and serves `/v1/models`; the engine logs `Registered CB rope state for instance N` after a completion; the completion returns HTTP 200. Pulls the PRIVATE payload image via a `dockerconfigjson` Secret built from `CACHEBLEND_REGISTRY_USER`/`CACHEBLEND_REGISTRY_TOKEN` — **Skips** if those are unset. |
|
|
|
|
### Prerequisites
|
|
|
|
What you need to install / configure before each target. The "Host
|
|
config" column is **only needed once per host** — subsequent runs
|
|
reuse it. Detailed rationale for each item lives in the per-target
|
|
sections below; this table is the quick-reference.
|
|
|
|
| Target | Tools | Host config | Cluster reqs |
|
|
|---|---|---|---|
|
|
| `test-e2e-kind` | `kind`, `kubectl`, `docker` | — | (cluster created fresh) |
|
|
| `test-e2e-cluster` | `kubectl` | — | `KUBECONFIG` → target cluster; pass `IMG=` (registry the cluster can pull from) |
|
|
| `test-e2e-gpu-kind` | `kind`, `kubectl`, `docker`, `helm` | NVIDIA driver + `nvidia-container-toolkit` installed, **plus** the two `nvidia-ctk` commands below | (cluster created fresh) |
|
|
| `test-e2e-gpu-cluster` | `kubectl`, `helm` | — | `KUBECONFIG` → GPU cluster; GPU node labeled `nvidia.com/gpu.present=true`; `nvidia` `RuntimeClass` installed; pass `IMG=` |
|
|
|
|
#### Tool install hints
|
|
|
|
```bash
|
|
# kind
|
|
go install sigs.k8s.io/kind/cmd/kind@latest
|
|
|
|
# kubectl — distro-specific; e.g. `curl -LO https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl`
|
|
# helm v3 — https://helm.sh/docs/intro/install/
|
|
# docker — distro-specific
|
|
```
|
|
|
|
#### GPU host one-time setup (only for `test-e2e-gpu-kind`)
|
|
|
|
```bash
|
|
# Configure docker to default to the nvidia runtime, then toggle the
|
|
# volume-mount-marker mechanism the inline Kind config uses to inject
|
|
# GPUs into the worker container. Restart docker once at the end.
|
|
sudo nvidia-ctk runtime configure --runtime=docker --set-as-default --cdi.enabled
|
|
sudo nvidia-ctk config --set accept-nvidia-visible-devices-as-volume-mounts=true --in-place
|
|
sudo systemctl restart docker
|
|
```
|
|
|
|
The target fails fast with a copy-pasteable fix command if either
|
|
piece of host config is missing.
|
|
|
|
**Not needed**: `nvkind`. An earlier iteration used it; the current
|
|
target uses the NVIDIA GPU Operator instead and gets GPU passthrough
|
|
via an inline Kind config. See `make/e2e-gpu.mk` for the rationale.
|
|
|
|
### Helper library (`test/utils/`)
|
|
|
|
| File | Purpose |
|
|
|---|---|
|
|
| `lmc.go` | `ApplyLMC`, `WaitLMCReconciled`, `WaitLMCPhase`, `WaitLMCReady`, `GetConnectionConfig` (typed parser), `PatchLMCSpec`, `DeleteLMCAndWaitGC` |
|
|
| `portforward.go` | `PortForward(spec, ports...)` — wraps `kubectl port-forward`, waits for the local port to accept TCP |
|
|
| `fixtures.go` | `go:embed`-backed fixture/golden loader |
|
|
| `runner.go` | `RunMake` / `RunFromOperator` — runs commands from the operator/ root **without** `os.Chdir` |
|
|
| `utils.go` | Legacy exec helpers retained for the kubebuilder-template Manager spec |
|
|
|
|
### Adding a new smoke spec
|
|
|
|
1. Drop the YAML under `test/utils/fixtures/<name>.yaml`.
|
|
2. Use `utils.NewLMCFromFixture(...)` to load and override `name`/`namespace`.
|
|
3. Call the helpers above; do **not** call `os.Chdir` or read paths
|
|
relative to the working directory — fixtures resolve via `go:embed`,
|
|
and shell commands accept their working directory through `cmd.Dir`.
|
|
4. Wrap the spec body with `recordOnFailure(nsName)` in `AfterEach` so
|
|
failures dump controller logs, events, pod descriptions, and the CR yaml.
|
|
|
|
### GPU tier (M2)
|
|
|
|
Two entry points share the same `e2e_gpu`-tagged specs:
|
|
|
|
#### `test-e2e-gpu-kind` — self-contained Kind cluster
|
|
|
|
Best when you have GPUs on the dev box and don't want to wire up an
|
|
external cluster. The target hand-rolls a Kind cluster config that
|
|
mounts `/dev/null` at `/var/run/nvidia-container-devices/all` in the
|
|
worker — combined with the host setup below,
|
|
nvidia-container-runtime sees that marker and injects all GPU
|
|
devices + driver libraries into the Kind worker container. Then the
|
|
target installs the
|
|
[NVIDIA GPU Operator](https://github.com/NVIDIA/gpu-operator), which
|
|
runs a toolkit daemonset that installs nvidia-container-toolkit
|
|
*inside the Kind node* and registers a `nvidia` containerd runtime
|
|
handler. After that, pods with `runtimeClassName: nvidia` (which the
|
|
LMCache DaemonSet and the test-side vLLM Deployment already use)
|
|
get NVML / libcuda injected. The cluster is auto-deleted on exit —
|
|
same trap pattern as `test-e2e-kind`. All in-cluster manifests
|
|
(including the Kind config) are inlined into the Makefile.
|
|
|
|
We explored several alternatives before settling on GPU Operator:
|
|
the bare device plugin alone fails with `Failed to initialize NVML:
|
|
ERROR_LIBRARY_NOT_FOUND` because pods scheduled by Kind's inner
|
|
containerd don't inherit the worker's library mounts; manually
|
|
apt-installing the toolkit via `docker exec` works but breaks if
|
|
the kindest/node image changes; nvkind didn't reliably produce
|
|
GPU-passthrough markers on the target host. GPU Operator does the
|
|
toolkit install as a Kubernetes-native DaemonSet, which is the most
|
|
robust path. Trade-off: helm install takes ~10 min (operator +
|
|
ClusterPolicy + 5 daemonsets), versus ~30 s for the bare plugin
|
|
when it works.
|
|
|
|
Host one-time setup (NOT automated):
|
|
|
|
```bash
|
|
# 1. NVIDIA driver + nvidia-container-toolkit installed (distro-specific).
|
|
|
|
# 2. Configure docker + nvidia-container-runtime:
|
|
sudo nvidia-ctk runtime configure --runtime=docker --set-as-default --cdi.enabled
|
|
sudo nvidia-ctk config --set accept-nvidia-visible-devices-as-volume-mounts=true --in-place
|
|
sudo systemctl restart docker
|
|
|
|
# 3. helm + kubectl + kind on PATH.
|
|
```
|
|
|
|
Then:
|
|
|
|
```bash
|
|
make test-e2e-gpu-kind
|
|
```
|
|
|
|
The Makefile target fails fast with a copy-pasteable fix command if
|
|
either `Default Runtime: nvidia` is missing from `docker info` or
|
|
`accept-nvidia-visible-devices-as-volume-mounts=true` is missing
|
|
from `/etc/nvidia-container-runtime/config.toml`.
|
|
|
|
Single-node is intentional: the LMCache DaemonSet and the test-side
|
|
vLLM Deployment both schedule onto the same (only) worker, which is
|
|
what the kv-cache transfer needs anyway (hostIPC + cudaIPC require
|
|
colocation).
|
|
|
|
**Side effect of step 2 to be aware of**: after the flip, every
|
|
docker container on the host — not just Kind workers — starts
|
|
through nvidia-container-runtime. Non-GPU workloads still work but
|
|
go through one extra hook on startup.
|
|
|
|
The `nvidia` RuntimeClass is registered by nvkind. Pods that need
|
|
GPU access (the LMCache DaemonSet, the test-side vLLM Deployment)
|
|
already reference `runtimeClassName: nvidia`.
|
|
|
|
#### `test-e2e-gpu-cluster` — existing GPU cluster
|
|
|
|
Use when targeting OpenShift / EKS / GKE GPU clusters. Prerequisites:
|
|
|
|
1. At least one node has `nvidia.com/gpu.present=true` and the
|
|
`nvidia` RuntimeClass installed.
|
|
2. `KUBECONFIG` points at that cluster, the operator image is pushed
|
|
to a registry the cluster can pull from, and `IMG=` references it.
|
|
3. The cluster can pull `lmcache/vllm-openai:latest` (default for
|
|
both the LMCache DaemonSet and the test-side vLLM workload).
|
|
Override with `VLLM_IMAGE=` if you mirror it elsewhere.
|
|
4. Internet egress for Hugging Face model downloads, OR the model is
|
|
already on the node. Default model is `Qwen/Qwen2.5-0.5B`; override
|
|
with `VLLM_MODEL=<org>/<model>`.
|
|
5. To run only the `/conf` spec and skip the heavyweight vLLM
|
|
round-trip, set `SKIP_VLLM_INTEGRATION=true`.
|
|
|
|
Timeout is 60 min — cold image pulls + model download routinely eat
|
|
20+ min before the first inference.
|
|
|
|
#### CacheBlend integration knobs
|
|
|
|
`cacheblend_integration_smoke_test.go` pulls a **private** payload image
|
|
(`tensormesh/cacheblend-plugin:latest-nightly`). It builds a
|
|
`dockerconfigjson` pull Secret in the test namespace from env credentials, so
|
|
set these (in Buildkite: pipeline secrets, same mechanism as `HF_TOKEN`):
|
|
|
|
- `CACHEBLEND_REGISTRY_USER` / `CACHEBLEND_REGISTRY_TOKEN` — registry username +
|
|
read-only PAT. **Unset ⇒ the spec Skips** (keeps credential-less clusters green).
|
|
- `CACHEBLEND_REGISTRY_SERVER` — registry server (default
|
|
`https://index.docker.io/v1/` for Docker Hub).
|
|
- `CACHEBLEND_PAYLOAD_IMAGE` — override the private plugin image (default
|
|
`tensormesh/cacheblend-plugin:latest-nightly`).
|
|
- `CACHEBLEND_ENGINE_IMAGE` — blend server image (default
|
|
`lmcache/vllm-openai:latest-nightly`). `VLLM_IMAGE` (also defaults to
|
|
`latest-nightly` for this spec) / `VLLM_MODEL` apply as above. Engine, vLLM,
|
|
and the latest-nightly payload plugin must share a compatibility window.
|
|
- `CACHEBLEND_BACKEND_LOG_PATTERN` — regex proving vLLM loaded the CUSTOM
|
|
attention backend (default `Using AttentionBackendEnum\.CUSTOM backend`).
|
|
- `SKIP_CACHEBLEND_INTEGRATION=true` — skip this spec.
|
|
|
|
---
|
|
|
|
## Project Structure
|
|
|
|
**Single-group layout (default):**
|
|
```
|
|
cmd/main.go Manager entry (registers controllers/webhooks)
|
|
api/<version>/*_types.go CRD schemas (+kubebuilder markers)
|
|
api/<version>/zz_generated.* Auto-generated (DO NOT EDIT)
|
|
internal/controller/* Reconciliation logic
|
|
internal/webhook/* Validation/defaulting (if present)
|
|
config/crd/bases/* Generated CRDs (DO NOT EDIT)
|
|
config/rbac/role.yaml Generated RBAC (DO NOT EDIT)
|
|
config/samples/* Example CRs (edit these)
|
|
Makefile Top-level orchestrator: vars + `include make/*.mk`
|
|
make/*.mk Targets by concern (dev / e2e / e2e-gpu / lint / build / deploy / tools)
|
|
PROJECT Kubebuilder metadata Auto-generated (DO NOT EDIT)
|
|
```
|
|
|
|
**Multi-group layout** (for projects with multiple API groups):
|
|
```
|
|
api/<group>/<version>/*_types.go CRD schemas by group
|
|
internal/controller/<group>/* Controllers by group
|
|
internal/webhook/<group>/<version>/* Webhooks by group and version (if present)
|
|
```
|
|
|
|
Multi-group layout organizes APIs by group name (e.g., `batch`, `apps`). Check the `PROJECT` file for `multigroup: true`.
|
|
|
|
**To convert to multi-group layout:**
|
|
1. Run: `kubebuilder edit --multigroup=true`
|
|
2. Move APIs: `mkdir -p api/<group> && mv api/<version> api/<group>/`
|
|
3. Move controllers: `mkdir -p internal/controller/<group> && mv internal/controller/*.go internal/controller/<group>/`
|
|
4. Move webhooks (if present): `mkdir -p internal/webhook/<group> && mv internal/webhook/<version> internal/webhook/<group>/`
|
|
5. Update import paths in all files
|
|
6. Fix `path` in `PROJECT` file for each resource
|
|
7. Update test suite CRD paths (add one more `..` to relative paths)
|
|
|
|
## Critical Rules
|
|
|
|
### Never Edit These (Auto-Generated)
|
|
- `config/crd/bases/*.yaml` - from `make manifests`
|
|
- `config/rbac/role.yaml` - from `make manifests`
|
|
- `config/webhook/manifests.yaml` - from `make manifests`
|
|
- `**/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
|