Gorbe is a Kubernetes admission webhook that enforces per-pod resource constraints using a custom CRD called GorbePolicy. Unlike LimitRange — which applies uniformly to every pod in a namespace — Gorbe uses label selectors so each policy only targets the pods you choose.
Two admission webhooks run in the same server:
| Webhook | Path | What it does |
|---|---|---|
| Mutating | /mutate |
Injects default CPU/memory/ephemeralStorage into pods that match a policy and leave those fields unset |
| Validating | /validate |
Rejects pods whose resources violate the min or max values of any matching policy |
- How it works
- Prerequisites
- Running unit tests
- Generating CRD manifests
- Running integration tests with kind
- Deploying to a real cluster
- GorbePolicy API reference
- Usage example
Pod CREATE / UPDATE
│
▼
┌───────────────────────┐
│ Mutating webhook │ /mutate
│ - fetch policies │ (from informer cache)
│ - match selectors │
│ - aggregate limits │ min = max(all mins)
│ - inject defaults │ max = min(all maxes)
└───────────────────────┘ default = min(all defaults)
│
▼
┌───────────────────────┐
│ Validating webhook │ /validate
│ - same matching │
│ - check min / max │ init containers: each individually
│ - reject if violated │ app containers: sum validated together
└───────────────────────┘
Aggregation rule when multiple selectors match one pod:
| Field | Rule |
|---|---|
min |
max of all minimums (most restrictive floor) |
max |
min of all maximums (most restrictive ceiling) |
default |
min of all defaults (most conservative default) |
| Tool | Purpose |
|---|---|
| Docker + Docker Compose | Building, testing, and running everything |
| kind | Local integration testing (creating a Kubernetes cluster in Docker) |
| kubectl | Talking to the kind cluster during integration tests |
| openssl | TLS cert generation for integration test setup |
All Go commands (build, test, lint, code generation) run inside a Docker container via docker compose run. You do not need Go installed on your machine.
Install kind: https://kind.sigs.k8s.io/docs/user/quick-start/#installation
Unit tests cover the policy engine (aggregation, mutation, validation), webhook handlers, and the server lifecycle. No cluster is needed.
make testThis runs go fmt, go vet, and go test -race inside the Docker container. A coverage report is written to cover.out.
You can also run individual steps:
make fmt # go fmt
make vet # go vetThe CRD YAML at config/crd/bases/ is generated from the Go type annotations in pkg/api/v1alpha1/ using controller-gen. The tool is installed automatically inside the container.
# Regenerate the CRD manifest
make manifests
# Regenerate DeepCopy methods
make generateThe generated CRD file will appear at config/crd/bases/.
Integration tests create a real kind cluster, build and load the Docker image, deploy the webhook with TLS, and assert mutation and rejection behaviour end-to-end.
In addition to Docker, you need kind, kubectl, and openssl installed on your machine.
make test-integrationThis builds the Docker image, runs kind-setup.sh (creates cluster, deploys gorbe), then runs run-tests.sh.
# 1. Build image, create kind cluster, deploy gorbe
make test-integration-setup
# 2. Run only the test assertions (useful when iterating)
make test-integration-run
# 3. Tear down when done
make test-integration-clean| Test | Scenario | Expected |
|---|---|---|
| 1a | Pod matches test-defaults policy, no CPU set |
CPU 100m injected |
| 1b | Pod matches test-defaults policy, no memory set |
Memory 64Mi injected |
| 2 | Pod matches test-bounded policy, resources within bounds |
Admitted |
| 3 | Pod matches test-bounded policy, CPU 300m > max 200m |
Rejected |
| 4 | Pod matches test-bounded policy, memory 8Mi < min 32Mi |
Rejected |
| 5 | Pod has no label matching any policy | Admitted, no resources injected |
Test policies and pod fixtures live in test/integration/manifests/.
make docker-build IMG=your-registry/gorbe:v1.0.0
make docker-push IMG=your-registry/gorbe:v1.0.0The webhook server needs a TLS certificate trusted by kube-apiserver. The recommended approach is cert-manager:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: gorbe-webhook-cert
namespace: gorbe-system
spec:
secretName: gorbe-webhook-cert
dnsNames:
- gorbe-webhook.gorbe-system.svc
- gorbe-webhook.gorbe-system.svc.cluster.local
issuerRef:
name: your-cluster-issuer
kind: ClusterIssuerAlternatively, generate a self-signed cert:
openssl genrsa -out tls.key 2048
cat > san.conf <<EOF
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
prompt = no
[req_distinguished_name]
CN = gorbe-webhook.gorbe-system.svc
[v3_req]
keyUsage = keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = gorbe-webhook.gorbe-system.svc
DNS.2 = gorbe-webhook.gorbe-system.svc.cluster.local
EOF
openssl req -new -key tls.key -out tls.csr -config san.conf
openssl x509 -req -in tls.csr -signkey tls.key -out tls.crt \
-days 365 -extensions v3_req -extfile san.conf
kubectl create namespace gorbe-system
kubectl create secret tls gorbe-webhook-cert \
--cert=tls.crt --key=tls.key -n gorbe-system# Install the CRD
kubectl apply -f config/crd/bases/gorbe.io_gorbepolicies.yaml
# Create namespace and RBAC
kubectl apply -f config/rbac/rbac.yaml
# Deploy the webhook server (update the image reference first)
sed -i 's|gorbe:latest|your-registry/gorbe:v1.0.0|' deploy/deployment.yaml
kubectl apply -f deploy/deployment.yaml
# Wait for the pod to be ready
kubectl rollout status deployment/gorbe-webhook -n gorbe-system --timeout=120s
# Register the webhook configurations (patch caBundle with your CA cert)
CA_BUNDLE=$(base64 < tls.crt | tr -d '\n')
sed "s/caBundle:.*/caBundle: ${CA_BUNDLE}/" config/webhook/mutating.yaml | kubectl apply -f -
sed "s/caBundle:.*/caBundle: ${CA_BUNDLE}/" config/webhook/validating.yaml | kubectl apply -f -kubectl get pods -n gorbe-system
kubectl get mutatingwebhookconfigurations gorbe-mutating-webhook
kubectl get validatingwebhookconfigurations gorbe-validating-webhook
# Create a test policy and pod
kubectl apply -f - <<EOF
apiVersion: gorbe.io/v1alpha1
kind: GorbePolicy
metadata:
name: test
namespace: default
spec:
podSelectors:
- selector:
matchLabels:
app: myapp
resources:
cpu:
default: "200m"
memory:
default: "128Mi"
EOF
kubectl run myapp --image=nginx --labels=app=myapp
kubectl get pod myapp -o jsonpath='{.spec.containers[0].resources}' | python3 -m json.toolkubectl delete mutatingwebhookconfiguration gorbe-mutating-webhook
kubectl delete validatingwebhookconfiguration gorbe-validating-webhook
kubectl delete -f deploy/deployment.yaml
kubectl delete -f config/rbac/rbac.yaml
kubectl delete -f config/crd/bases/gorbe.io_gorbepolicies.yaml
kubectl delete namespace gorbe-systemapiVersion: gorbe.io/v1alpha1
kind: GorbePolicy
Scope: Namespaced. A policy only affects pods in the same namespace.
| Field | Type | Required | Description |
|---|---|---|---|
selector |
LabelSelector |
yes | Kubernetes label selector. Supports matchLabels and matchExpressions. |
resources |
ResourceConstraints |
yes | Constraints to apply to matching pods. |
| Field | Type | Description |
|---|---|---|
cpu |
ResourceSpec |
CPU constraints |
memory |
ResourceSpec |
Memory constraints |
ephemeralStorage |
ResourceSpec |
Ephemeral storage constraints |
All three are optional. Omitting a field means no constraint is applied for that resource type.
| Field | Type | Description |
|---|---|---|
min |
quantity | Minimum allowed value. Pods with a lower request or limit are rejected. |
max |
quantity | Maximum allowed value. Pods with a higher request or limit are rejected. |
default |
quantity | Default applied to both requests and limits when the container does not set that resource. Existing values are never overwritten. |
Quantities use standard Kubernetes notation: 100m, 500m, "1", "2" for CPU; 128Mi, 1Gi for memory.
Validation semantics:
- Init containers are validated individually (they run one at a time).
- Regular containers are validated as a sum (they all run simultaneously).
apiVersion: gorbe.io/v1alpha1
kind: GorbePolicy
metadata:
name: team-a-policy
namespace: team-a
spec:
podSelectors:
# All pods in this namespace get at least a sensible default
- selector:
matchLabels:
team: a
resources:
cpu:
default: "200m"
memory:
default: "256Mi"
# Production pods have tighter bounds
- selector:
matchLabels:
team: a
env: prod
resources:
cpu:
min: "100m"
max: "4"
memory:
min: "128Mi"
max: "8Gi"A pod with labels team: a and env: prod matches both selectors. The aggregated constraints will be:
- CPU default:
200m, CPU min:100m, CPU max:4 - Memory default:
256Mi, Memory min:128Mi, Memory max:8Gi
A pod with only team: a matches the first selector only and gets defaults injected without any min/max enforcement.