claude-skills/engineering/skills/kubernetes-operator/references/operator_pattern.md
Claude 6c16309801
feat(skills): ship kubernetes-operator (Phase 2 — operator pattern discipline)
Phase 2 of the multi-skill build effort. Same 14-step pipeline as Phase 1.

## What landed

### New skill: engineering/kubernetes-operator

End-to-end Kubernetes Operator discipline. Published as BOTH:
- Standalone plugin: engineering/kubernetes-operator/
- Bundled mirror:    engineering/skills/kubernetes-operator/

3 stdlib-only Python tools:
- crd_validator.py            — checks CRD YAMLs for status subresource,
                                 structural schema, conditions array, printer
                                 columns, version policy, scope
- reconcile_lint.py            — finds reconcile-loop bugs in Go: time.Sleep,
                                 spec mutation via r.Update, missing requeue,
                                 oversized reconcile bodies, panic/os.Exit,
                                 unbalanced finalizer add/remove
- operator_capability_audit.py — scores against OperatorHub Capability
                                 Levels 1-5 with concrete next-level steps

4 reference docs:
- operator_pattern.md   — what an operator IS, when to use vs Helm/Deployment
- crd_design.md         — anatomy of a production CRD, versioning, conversion
- reconcile_loop.md     — idempotence patterns, error/requeue, status subresource
- tooling_landscape.md  — controller-runtime / kubebuilder / operator-sdk /
                          metacontroller / KOPF / java-operator-sdk decision tree

Asset templates:
- crd_template.yaml       — passes crd_validator.py PASS-clean
- reconcile_skeleton.go   — passes reconcile_lint.py PASS-clean

Plus: SKILL.md (213 lines), README.md, /operator-audit slash command.

### Audit verdict (evidence-based)

Closest existing coverage:
- engineering-team/senior-devops — kubectl / blue-green deploys, no operators
- engineering/helm-chart-builder — Helm charts (different abstraction)
- engineering-team/cloud-security — k8s RBAC at high level
None cover the Operator pattern (CRD + controller + reconcile loop).
Verdict: BUILD. Gap is real and tooling-shaped.

### Self-test (meta-validation)

During build, the new linters caught 4 real bugs in their own asset templates:
- crd_validator.py wrongly anchored regexes to start-of-line, misclassifying
  indented YAML keys (scope, singular, listKind) as missing
- reconcile_lint.py checked finalizer add/remove balance per-function,
  missing the cross-function pattern in the asset (Add in main reconcile,
  Remove in reconcileDelete)
Both linters fixed; assets re-tested; both PASS clean.

This is Karpathy principle 4 in action: verifiable goals catch real bugs.

### Marketplace / registry

- marketplace.json: kubernetes-operator registered as standalone plugin
- engineering-advanced-skills bundle: 45 → 46 → 47 skills, version → 2.4.1
- engineering/.claude-plugin/plugin.json: version + skill list updated
- mkdocs.yml: nav entry under "Engineering - POWERFUL"
- docs/skills/engineering/kubernetes-operator.md: docs page (manual,
  pending generate-docs.py classification fix)
- docs/commands/operator-audit.md: auto-generated
- .codex/, .gemini/: synced

### Karpathy-coder gates

- complexity_checker (strict): 85/100 average, depth-4-to-6 WARNs (lambdas
  in capability audit). Same range as karpathy-coder's own scripts (70/100
  baseline). Verdict: WARN, not FAIL.
- All 1648 tests pass (was 1630; added 18 for the new skill).
- mkdocs build --strict: succeeded in 14.44s.

### Verifiable success criteria (all green)

✓  scripts/*.py --help     → exit 0 for all 3 scripts
✓  SKILL.md frontmatter    → name + description + tags + compatible_tools
✓  plugin.json schema      → 8 fields exact (verified by check_plugin_json.py)
✓  sync_skill_bundles      → standalone ↔ bundled mirror in sync
✓  marketplace.json        → standalone entry + bundle counts updated
✓  generate-docs.py        → command page generated (skill page manual)
✓  mkdocs build --strict   → succeeded
✓  cross-tool sync         → codex + gemini synced
✓  pytest tests/           → 1648 passed, 0 failed
✓  CHANGELOG.md            → [Unreleased] entry expanded
✓  Self-test               → linters caught + fixed 4 real bugs in own assets

## Files

- engineering/kubernetes-operator/                              (new standalone plugin)
- engineering/skills/kubernetes-operator/                       (new bundled mirror)
- commands/operator-audit.md                                    (new slash command)
- docs/skills/engineering/kubernetes-operator.md                (new docs page)
- docs/commands/operator-audit.md                               (auto-generated)
- mkdocs.yml                                                    (nav entries)
- .claude-plugin/marketplace.json                               (registered)
- engineering/.claude-plugin/plugin.json                        (bundle bumped)
- CHANGELOG.md                                                  ([Unreleased] expanded)
- .codex/, .gemini/                                             (cross-tool sync)

https://claude.ai/code/session_01Dq12xJakFRxwaoU8Pqejdm
2026-05-09 09:01:45 +00:00

7 KiB

The operator pattern

An operator is a controller that reconciles a Custom Resource (CR) toward its declared spec. It encodes operational knowledge — installation, upgrades, backups, failover — that would otherwise live in tribal knowledge or runbooks.

When you need an operator

Build an operator when:

  • The application has nontrivial lifecycle operations (backup, restore, version upgrade, failover) that go beyond a simple Deployment
  • The application has statefulness or topology that Helm/Deployment can't express (leader election, peer discovery, rolling state migration)
  • Multiple teams need to provision instances of the application via a Kubernetes API, not a custom UI
  • The application's operational discipline is documented in runbooks but unevenly applied

Don't build an operator when:

  • A Helm chart is enough (most stateless apps fit here)
  • A CronJob can run the operational task on a schedule
  • The custom logic is a one-time migration (use a Job)
  • Three engineers can manage it via Deployment + ConfigMap

Operator pattern shape

┌────────────────────────────────────────────────────────┐
│  apiVersion: apps.example.com/v1alpha1                 │
│  kind: MyApp                       ← Custom Resource   │
│  spec:                                                 │
│    replicas: 3                     ← user's intent     │
│    version: 1.4.2                                      │
│  status:                                               │
│    conditions:                     ← controller's view │
│      - type: Ready                                     │
│        status: "True"                                  │
│    phase: Running                                      │
└────────────────────────────────────────────────────────┘
                       ↑
                       │ owns
                       │
┌────────────────────────────────────────────────────────┐
│  controller.Reconcile(ctx, req) ⟶ ctrl.Result, error   │
│   1. read CR (the spec) from the cache                 │
│   2. read actual state (Pods, Services, ConfigMaps)    │
│   3. diff actual against desired                       │
│   4. act idempotently to converge                      │
│   5. update status with observed state                 │
│   6. return RequeueAfter or done                       │
└────────────────────────────────────────────────────────┘

Reconcile runs whenever:

  • The CR changes
  • A child resource changes
  • A periodic resync fires (default 10h, configurable)
  • An explicit requeue from a previous run

Spec vs status — the cardinal split

spec status
Authored by the user Authored by the controller
Mutable through kubectl edit Mutable only via the status subresource
Captures intent Captures observed reality
Triggers reconcile Does NOT trigger reconcile (when subresource is enabled)

Violating the split is the #1 cause of operator bugs:

  • Mutating spec from the controller → user changes get overwritten
  • Updating status without the subresource → status update triggers spec reconcile → loop

Reconcile must be idempotent

Reconcile is called repeatedly for the same state. The function must:

  • Produce the same outcome regardless of call count
  • Use Create-or-Update patterns (controllerutil.CreateOrUpdate)
  • Compare current state to desired before writing
  • Never assume "this is the first time we've seen this resource"

Idempotence test: if reconcile is called 100 times in a row with the same spec and no external change, the system must converge after the first call and do nothing on the next 99.

OwnerReferences and cascading deletion

Every child resource the operator creates must have its OwnerReferences set to the parent CR. Then:

  • Deleting the CR deletes children automatically
  • The garbage collector handles orphan cleanup
  • The operator doesn't need explicit teardown logic for owned resources

External resources (cloud DBs, S3 buckets, DNS records) don't have OwnerReferences. Use finalizers to clean them up.

Finalizers

A finalizer blocks deletion until the controller has cleaned up external state.

1. User: kubectl delete myapp foo
2. API server: sets metadata.deletionTimestamp; does NOT delete
3. Controller: sees deletionTimestamp; does cleanup; removes finalizer
4. API server: deletion now proceeds

Without a finalizer, external resources orphan. With one, the controller has a guaranteed hook to run cleanup before the CR disappears.

Conditions

The standard pattern for status reporting:

status:
  conditions:
    - type: Ready          # type values are operator-defined
      status: "True"        # True | False | Unknown
      reason: "AllReady"    # PascalCase, programmatic
      message: "All replicas ready"  # human-readable
      lastTransitionTime: "2026-05-08T12:00:00Z"
    - type: Reconciling
      status: "False"
      reason: "Idle"
      lastTransitionTime: "2026-05-08T12:00:00Z"

Use meta/v1.Conditions and meta/v1.SetStatusCondition from kubebuilder/controller-runtime — don't roll your own.

Webhooks

Two types:

  • ValidatingWebhook — reject invalid CRs at admission (better than failing in reconcile)
  • MutatingWebhook — fill in defaults / inject sidecars (use sparingly; surprising side effects)

Run webhooks in the same controller binary or a sidecar; cert-manager rotates the certs.

Anti-patterns

  • Imperative reconcile: "if event = create, do X; if event = update, do Y". Wrong shape. Reconcile = make actual=desired regardless of how we got here.
  • No status subresource: status updates re-trigger reconcile.
  • Status mutation in many places: centralize in a setStatus helper.
  • Reconcile depending on event order: events can be missed; reconcile must converge from any starting state.
  • Long reconcile (>2 min): blocks the work queue; split work via RequeueAfter.

Decision flow: when an operator is the right answer

Need: I want to manage <X> in Kubernetes.

Is <X> a stateless web app?           → Deployment + Service. Done.
Is <X> a stateless web app with config?  → Deployment + ConfigMap.
Need version upgrade automation?       → Helm. Done.
Need stateful behaviour (leader, peers)? → StatefulSet.
Need application-aware operations
  (backup, version migration, repair)?  → Operator.
Need to expose <X> as a k8s resource
  to other teams?                       → Operator.

When in doubt: start with Helm. Move to an operator only when Helm can't express the operational logic.