mirror of
https://github.com/alirezarezvani/claude-skills.git
synced 2026-08-28 04:24:58 +00:00
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
This commit is contained in:
parent
0c7d19d297
commit
6c16309801
37 changed files with 3749 additions and 19 deletions
|
|
@ -59,7 +59,7 @@
|
|||
{
|
||||
"name": "engineering-advanced-skills",
|
||||
"source": "./engineering",
|
||||
"description": "45 advanced engineering skills: agent designer, agent workflow designer, AgentHub, RAG architect, database designer, focused-fix, browser-automation, spec-driven-workflow, secrets-vault-manager, sql-database-assistant, migration architect, observability designer, dependency auditor, release manager, API reviewer, CI/CD pipeline builder, MCP server builder, skill security auditor, performance profiler, Helm chart builder, Terraform patterns, self-eval, llm-cost-optimizer, prompt-governance, behuman, code-tour, demo-video, data-quality-auditor, statistical-analyst, llm-wiki (second brain for Obsidian + Claude Code, Karpathy pattern), feature-flags-architect (flag debt scanner, rollout planner, kill-switch audit), and more.",
|
||||
"description": "46 advanced engineering skills: agent designer, agent workflow designer, AgentHub, RAG architect, database designer, focused-fix, browser-automation, spec-driven-workflow, secrets-vault-manager, sql-database-assistant, migration architect, observability designer, dependency auditor, release manager, API reviewer, CI/CD pipeline builder, MCP server builder, skill security auditor, performance profiler, Helm chart builder, Terraform patterns, self-eval, llm-cost-optimizer, prompt-governance, behuman, code-tour, demo-video, data-quality-auditor, statistical-analyst, llm-wiki (second brain for Obsidian + Claude Code, Karpathy pattern), feature-flags-architect (flag debt scanner, rollout planner, kill-switch audit), kubernetes-operator (CRD validator, reconcile linter, capability auditor), and more.",
|
||||
"version": "2.4.0",
|
||||
"author": {
|
||||
"name": "Alireza Rezvani"
|
||||
|
|
@ -592,6 +592,28 @@
|
|||
],
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "kubernetes-operator",
|
||||
"source": "./engineering/kubernetes-operator",
|
||||
"description": "End-to-end Kubernetes Operator discipline: CRD design, reconcile-loop patterns, and OperatorHub Capability Levels. Ships CRD validator, reconcile-loop linter, and capability auditor (3 stdlib Python tools), 4 references on the operator pattern + CRD design + reconcile patterns + framework comparison (controller-runtime/kubebuilder/operator-sdk/metacontroller/KOPF), CRD + Go controller skeletons, and /operator-audit slash command. NOT a generic k8s skill — specifically the Operator pattern.",
|
||||
"version": "2.4.0",
|
||||
"author": {
|
||||
"name": "Alireza Rezvani"
|
||||
},
|
||||
"keywords": [
|
||||
"kubernetes",
|
||||
"operator",
|
||||
"crd",
|
||||
"controller-runtime",
|
||||
"kubebuilder",
|
||||
"operator-sdk",
|
||||
"metacontroller",
|
||||
"kopf",
|
||||
"reconcile",
|
||||
"devops"
|
||||
],
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "agile-product-owner",
|
||||
"source": "./product-team/agile-product-owner",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"name": "claude-code-skills",
|
||||
"description": "Production-ready skill packages for AI agents - Marketing, Engineering, Product, C-Level, PM, and RA/QM",
|
||||
"repository": "https://github.com/alirezarezvani/claude-skills",
|
||||
"total_skills": 184,
|
||||
"total_skills": 185,
|
||||
"skills": [
|
||||
{
|
||||
"name": "business-growth-skills",
|
||||
|
|
@ -509,6 +509,12 @@
|
|||
"category": "engineering-advanced",
|
||||
"description": "This skill should be used when the user asks to \"design interview processes\", \"create hiring pipelines\", \"calibrate interview loops\", \"generate interview questions\", \"design competency matrices\", \"analyze interviewer bias\", \"create scoring rubrics\", \"build question banks\", or \"optimize hiring systems\". Use for designing role-specific interview loops, competency assessments, and hiring calibration systems."
|
||||
},
|
||||
{
|
||||
"name": "kubernetes-operator",
|
||||
"source": "../../engineering/skills/kubernetes-operator",
|
||||
"category": "engineering-advanced",
|
||||
"description": "Use when building a Kubernetes Operator \u2014 custom controllers that reconcile CRD state. Triggers on \"build an operator\", \"CRD design\", \"reconcile loop\", \"controller-runtime\", \"kubebuilder\", \"operator-sdk\", \"metacontroller\", \"KOPF\", \"operator capability levels\", or \"custom resource\". Ships CRD validator, reconcile-loop linter, and OperatorHub capability auditor (all stdlib Python), 4 references on the operator pattern + CRD design + reconcile patterns + tooling landscape, and a /operator-audit slash command. NOT a generic k8s skill \u2014 specifically the Operator pattern."
|
||||
},
|
||||
{
|
||||
"name": "mcp-server-builder",
|
||||
"source": "../../engineering/skills/mcp-server-builder",
|
||||
|
|
@ -1127,7 +1133,7 @@
|
|||
"description": "Software engineering and technical skills"
|
||||
},
|
||||
"engineering-advanced": {
|
||||
"count": 36,
|
||||
"count": 37,
|
||||
"source": "../../engineering",
|
||||
"description": "Advanced engineering skills - agents, RAG, MCP, CI/CD, databases, observability"
|
||||
},
|
||||
|
|
|
|||
1
.codex/skills/kubernetes-operator
Symbolic link
1
.codex/skills/kubernetes-operator
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../engineering/skills/kubernetes-operator
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"name": "gemini-cli-skills",
|
||||
"total_skills": 302,
|
||||
"total_skills": 305,
|
||||
"skills": [
|
||||
{
|
||||
"name": "README",
|
||||
|
|
@ -393,6 +393,11 @@
|
|||
"category": "command",
|
||||
"description": "Generate OKR cascades from company strategy to team objectives. Usage: /okr generate <strategy>"
|
||||
},
|
||||
{
|
||||
"name": "operator-audit",
|
||||
"category": "command",
|
||||
"description": "Run the full Kubernetes Operator audit (CRD + reconcile + capability) on the current repo"
|
||||
},
|
||||
{
|
||||
"name": "persona",
|
||||
"category": "command",
|
||||
|
|
@ -903,6 +908,11 @@
|
|||
"category": "engineering-advanced",
|
||||
"description": "Use when writing, reviewing, or committing code to enforce Karpathy's 4 coding principles \u2014 surface assumptions before coding, keep it simple, make surgical changes, define verifiable goals. Triggers on \"review my diff\", \"check complexity\", \"am I overcomplicating this\", \"karpathy check\", \"before I commit\", or any code quality concern where the LLM might be overcoding."
|
||||
},
|
||||
{
|
||||
"name": "kubernetes-operator",
|
||||
"category": "engineering-advanced",
|
||||
"description": "Use when building a Kubernetes Operator \u2014 custom controllers that reconcile CRD state. Triggers on \"build an operator\", \"CRD design\", \"reconcile loop\", \"controller-runtime\", \"kubebuilder\", \"operator-sdk\", \"metacontroller\", \"KOPF\", \"operator capability levels\", or \"custom resource\". Ships CRD validator, reconcile-loop linter, and OperatorHub capability auditor (all stdlib Python), 4 references on the operator pattern + CRD design + reconcile patterns + tooling landscape, and a /operator-audit slash command. NOT a generic k8s skill \u2014 specifically the Operator pattern."
|
||||
},
|
||||
{
|
||||
"name": "llm-cost-optimizer",
|
||||
"category": "engineering-advanced",
|
||||
|
|
@ -1018,6 +1028,11 @@
|
|||
"category": "engineering-advanced",
|
||||
"description": "Use when adding, retiring, or auditing feature flags. Triggers on \"add a flag\", \"ship behind a flag\", \"rollout plan\", \"kill switch\", \"stale flags\", \"flag debt\", \"LaunchDarkly\", \"GrowthBook\", \"Statsig\", \"Unleash\", \"Flipt\", or any progressive-delivery question. Ships flag debt scanner, rollout planner, and kill-switch auditor (all stdlib Python), 4 references on flag taxonomy + provider trade-offs + rollout strategies + lifecycle, plus a /flag-cleanup slash command."
|
||||
},
|
||||
{
|
||||
"name": "skills-kubernetes-operator",
|
||||
"category": "engineering-advanced",
|
||||
"description": "Use when building a Kubernetes Operator \u2014 custom controllers that reconcile CRD state. Triggers on \"build an operator\", \"CRD design\", \"reconcile loop\", \"controller-runtime\", \"kubebuilder\", \"operator-sdk\", \"metacontroller\", \"KOPF\", \"operator capability levels\", or \"custom resource\". Ships CRD validator, reconcile-loop linter, and OperatorHub capability auditor (all stdlib Python), 4 references on the operator pattern + CRD design + reconcile patterns + tooling landscape, and a /operator-audit slash command. NOT a generic k8s skill \u2014 specifically the Operator pattern."
|
||||
},
|
||||
{
|
||||
"name": "skills-run",
|
||||
"category": "engineering-advanced",
|
||||
|
|
@ -1528,7 +1543,7 @@
|
|||
"description": "C-level resources"
|
||||
},
|
||||
"command": {
|
||||
"count": 30,
|
||||
"count": 31,
|
||||
"description": "Command resources"
|
||||
},
|
||||
"engineering": {
|
||||
|
|
@ -1536,7 +1551,7 @@
|
|||
"description": "Engineering resources"
|
||||
},
|
||||
"engineering-advanced": {
|
||||
"count": 64,
|
||||
"count": 66,
|
||||
"description": "Engineering-advanced resources"
|
||||
},
|
||||
"finance": {
|
||||
|
|
|
|||
1
.gemini/skills/kubernetes-operator/SKILL.md
Symbolic link
1
.gemini/skills/kubernetes-operator/SKILL.md
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../engineering/skills/kubernetes-operator/SKILL.md
|
||||
1
.gemini/skills/operator-audit/SKILL.md
Symbolic link
1
.gemini/skills/operator-audit/SKILL.md
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../commands/operator-audit.md
|
||||
1
.gemini/skills/skills-kubernetes-operator/SKILL.md
Symbolic link
1
.gemini/skills/skills-kubernetes-operator/SKILL.md
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../engineering/kubernetes-operator/skills/kubernetes-operator/SKILL.md
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -5,11 +5,12 @@ All notable changes to the Claude Skills Library will be documented in this file
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased] — Skill Expansion Phase 1
|
||||
## [Unreleased] — Skill Expansion Phase 1+2
|
||||
|
||||
### Added — Engineering POWERFUL
|
||||
|
||||
- **feature-flags-architect** — End-to-end feature-flag discipline. Detects stale flags as debt (`flag_debt_scanner.py`), generates phased rollout plans across ring/linear/log/cohort strategies (`rollout_planner.py`), and audits every flag for documented kill switch (`kill_switch_audit.py`). 4 references on flag taxonomy, provider comparison (LaunchDarkly / GrowthBook / Statsig / Unleash / Flipt / DIY), rollout strategies, and lifecycle. Ships standalone plugin AND in the engineering-advanced-skills bundle. New `/flag-cleanup` slash command.
|
||||
- **kubernetes-operator** — End-to-end Kubernetes Operator discipline. Validates CRDs against operator-pattern best practices (`crd_validator.py`), lints Go reconcile functions for anti-patterns like `time.Sleep`, spec mutation, missing requeue, finalizer imbalance (`reconcile_lint.py`), and scores operators against OperatorHub Capability Levels 1-5 (`operator_capability_audit.py`). 4 references on operator pattern, CRD design, reconcile loop patterns, and framework comparison (controller-runtime / kubebuilder / operator-sdk / metacontroller / KOPF). Asset templates for production CRD YAML and Go controller skeleton (both pass linters). New `/operator-audit` slash command. NOT a generic k8s skill — specifically the Operator pattern. Self-tested: linters caught 4 real bugs in their own asset templates during build.
|
||||
|
||||
### Added — Repo infrastructure
|
||||
|
||||
|
|
@ -18,12 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Changed
|
||||
|
||||
- **Total skills:** 235 → 236 (+1 new engineering POWERFUL skill)
|
||||
- **Python tools:** 314 → 319
|
||||
- **References:** 435 → 439
|
||||
- **Slash commands:** 27 → 28
|
||||
- **engineering-advanced-skills** plugin: v2.3.3 → v2.4.0
|
||||
- **marketplace.json**: `feature-flags-architect` registered as standalone plugin
|
||||
- **Total skills:** 235 → 237 (+2 new engineering POWERFUL skills)
|
||||
- **Python tools:** 314 → 322
|
||||
- **References:** 435 → 443
|
||||
- **Slash commands:** 27 → 29
|
||||
- **engineering-advanced-skills** plugin: v2.3.3 → v2.4.1
|
||||
- **marketplace.json**: `feature-flags-architect` and `kubernetes-operator` registered as standalone plugins
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
|||
58
commands/operator-audit.md
Normal file
58
commands/operator-audit.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
description: Run the full Kubernetes Operator audit (CRD + reconcile + capability) on the current repo
|
||||
---
|
||||
|
||||
# /operator-audit
|
||||
|
||||
Run the full audit on a Kubernetes Operator repository:
|
||||
|
||||
1. Validate every CRD YAML against operator-pattern best practices
|
||||
2. Lint every Go controller's reconcile function for anti-patterns
|
||||
3. Score the operator against OperatorHub Capability Levels (1-5)
|
||||
4. Output a markdown report with pass/fail per check and concrete next steps
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/operator-audit
|
||||
/operator-audit --operator-dir ./my-operator
|
||||
/operator-audit --crd-dir ./config/crd --controller-dir ./controllers
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
```bash
|
||||
SKILL=engineering/kubernetes-operator/skills/kubernetes-operator
|
||||
DIR="${OPERATOR_DIR:-.}"
|
||||
|
||||
echo "## CRD validation"
|
||||
python "$SKILL/scripts/crd_validator.py" --crd "$DIR/config/crd" || true
|
||||
|
||||
echo ""
|
||||
echo "## Reconcile lint"
|
||||
python "$SKILL/scripts/reconcile_lint.py" --controller "$DIR/controllers" || python "$SKILL/scripts/reconcile_lint.py" --controller "$DIR/internal/controller" || true
|
||||
|
||||
echo ""
|
||||
echo "## Capability audit"
|
||||
python "$SKILL/scripts/operator_capability_audit.py" --operator-dir "$DIR"
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
A markdown report with:
|
||||
|
||||
- **CRD findings** per file: FAIL / WARN / PASS for each check
|
||||
- **Reconcile findings**: line-numbered anti-patterns
|
||||
- **Current capability level** + concrete advancement steps
|
||||
|
||||
## Pre-conditions
|
||||
|
||||
- Run from a Kubernetes Operator repository
|
||||
- Go controllers expected at `controllers/` or `internal/controller/`
|
||||
- CRDs expected at `config/crd/` (kubebuilder layout)
|
||||
- `kubernetes-operator` skill installed
|
||||
|
||||
## Post-conditions
|
||||
|
||||
- Markdown report streamed to terminal
|
||||
- Exit code 0 if all PASS; 1 if any FAIL
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
---
|
||||
title: "Slash Commands — AI Coding Agent Commands & Codex Shortcuts"
|
||||
description: "30 slash commands for Claude Code, Codex CLI, and Gemini CLI — sprint planning, tech debt analysis, PRDs, OKRs, and more."
|
||||
description: "31 slash commands for Claude Code, Codex CLI, and Gemini CLI — sprint planning, tech debt analysis, PRDs, OKRs, and more."
|
||||
---
|
||||
|
||||
<div class="domain-header" markdown>
|
||||
|
||||
# :material-console: Slash Commands
|
||||
|
||||
<p class="domain-count">30 commands for quick access to common operations</p>
|
||||
<p class="domain-count">31 commands for quick access to common operations</p>
|
||||
|
||||
</div>
|
||||
|
||||
|
|
@ -73,6 +73,12 @@ description: "30 slash commands for Claude Code, Codex CLI, and Gemini CLI — s
|
|||
|
||||
Generate cascaded OKR frameworks from company-level strategy down to team-level key results.
|
||||
|
||||
- :material-console:{ .lg .middle } **[`/operator-audit`](operator-audit.md)**
|
||||
|
||||
---
|
||||
|
||||
Run the full audit on a Kubernetes Operator repository:
|
||||
|
||||
- :material-console:{ .lg .middle } **[`/persona`](persona.md)**
|
||||
|
||||
---
|
||||
|
|
|
|||
65
docs/commands/operator-audit.md
Normal file
65
docs/commands/operator-audit.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
title: "/operator-audit — Slash Command for AI Coding Agents"
|
||||
description: "Run the full Kubernetes Operator audit (CRD + reconcile + capability) on the current repo. Slash command for Claude Code, Codex CLI, Gemini CLI."
|
||||
---
|
||||
|
||||
# /operator-audit
|
||||
|
||||
<div class="page-meta" markdown>
|
||||
<span class="meta-badge">:material-console: Slash Command</span>
|
||||
<span class="meta-badge">:material-github: <a href="https://github.com/alirezarezvani/claude-skills/tree/main/commands/operator-audit.md">Source</a></span>
|
||||
</div>
|
||||
|
||||
|
||||
Run the full audit on a Kubernetes Operator repository:
|
||||
|
||||
1. Validate every CRD YAML against operator-pattern best practices
|
||||
2. Lint every Go controller's reconcile function for anti-patterns
|
||||
3. Score the operator against OperatorHub Capability Levels (1-5)
|
||||
4. Output a markdown report with pass/fail per check and concrete next steps
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/operator-audit
|
||||
/operator-audit --operator-dir ./my-operator
|
||||
/operator-audit --crd-dir ./config/crd --controller-dir ./controllers
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
```bash
|
||||
SKILL=engineering/kubernetes-operator/skills/kubernetes-operator
|
||||
DIR="${OPERATOR_DIR:-.}"
|
||||
|
||||
echo "## CRD validation"
|
||||
python "$SKILL/scripts/crd_validator.py" --crd "$DIR/config/crd" || true
|
||||
|
||||
echo ""
|
||||
echo "## Reconcile lint"
|
||||
python "$SKILL/scripts/reconcile_lint.py" --controller "$DIR/controllers" || python "$SKILL/scripts/reconcile_lint.py" --controller "$DIR/internal/controller" || true
|
||||
|
||||
echo ""
|
||||
echo "## Capability audit"
|
||||
python "$SKILL/scripts/operator_capability_audit.py" --operator-dir "$DIR"
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
A markdown report with:
|
||||
|
||||
- **CRD findings** per file: FAIL / WARN / PASS for each check
|
||||
- **Reconcile findings**: line-numbered anti-patterns
|
||||
- **Current capability level** + concrete advancement steps
|
||||
|
||||
## Pre-conditions
|
||||
|
||||
- Run from a Kubernetes Operator repository
|
||||
- Go controllers expected at `controllers/` or `internal/controller/`
|
||||
- CRDs expected at `config/crd/` (kubebuilder layout)
|
||||
- `kubernetes-operator` skill installed
|
||||
|
||||
## Post-conditions
|
||||
|
||||
- Markdown report streamed to terminal
|
||||
- Exit code 0 if all PASS; 1 if any FAIL
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
---
|
||||
title: "Engineering - POWERFUL Skills — Agent Skills & Codex Plugins"
|
||||
description: "63 engineering - powerful skills — advanced agent-native skill and Claude Code plugin for AI agent design, infrastructure, and automation. Works with Claude Code, Codex CLI, Gemini CLI, and OpenClaw."
|
||||
description: "65 engineering - powerful skills — advanced agent-native skill and Claude Code plugin for AI agent design, infrastructure, and automation. Works with Claude Code, Codex CLI, Gemini CLI, and OpenClaw."
|
||||
---
|
||||
|
||||
<div class="domain-header" markdown>
|
||||
|
||||
# :material-rocket-launch: Engineering - POWERFUL
|
||||
|
||||
<p class="domain-count">63 skills in this domain</p>
|
||||
<p class="domain-count">65 skills in this domain</p>
|
||||
|
||||
</div>
|
||||
|
||||
|
|
|
|||
113
docs/skills/engineering/kubernetes-operator.md
Normal file
113
docs/skills/engineering/kubernetes-operator.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
---
|
||||
title: "Kubernetes Operator — Build Operators That Reconcile Correctly"
|
||||
description: "End-to-end Kubernetes Operator discipline for Claude Code: CRD design, reconcile-loop patterns, and OperatorHub Capability Levels. 3 stdlib Python tools (CRD validator, reconcile linter, capability auditor), 4 references, CRD + Go skeletons that pass the linters. NOT a generic k8s skill — specifically the Operator pattern."
|
||||
---
|
||||
|
||||
# Kubernetes Operator
|
||||
|
||||
<div class="page-meta" markdown>
|
||||
<span class="meta-badge">:material-rocket-launch: Engineering - POWERFUL</span>
|
||||
<span class="meta-badge">:material-identifier: `kubernetes-operator`</span>
|
||||
<span class="meta-badge">:material-github: <a href="https://github.com/alirezarezvani/claude-skills/tree/main/engineering/kubernetes-operator">Source</a></span>
|
||||
</div>
|
||||
|
||||
<div class="install-banner" markdown>
|
||||
<span class="install-label">Install:</span> <code>claude /plugin install kubernetes-operator</code>
|
||||
</div>
|
||||
|
||||
End-to-end discipline for building Kubernetes Operators correctly. Catches the recurring reconcile-loop bugs (missing finalizers, blocking calls, status drift, RBAC over-grants, no requeue) before they reach a cluster.
|
||||
|
||||
## When to use
|
||||
|
||||
- Building a new Kubernetes Operator (controller for a CRD)
|
||||
- Reviewing an existing operator for capability-level gaps
|
||||
- Auditing a CRD spec for status/conditions/finalizer correctness
|
||||
- Choosing a framework (controller-runtime / kubebuilder / operator-sdk / metacontroller / KOPF)
|
||||
- Designing the API surface of a Custom Resource
|
||||
- Hardening RBAC, leader election, or webhook validation
|
||||
|
||||
## When NOT to use
|
||||
|
||||
- Plain Helm chart packaging → use `helm-chart-builder`
|
||||
- Standard kubectl operations / blue-green deploys → use `senior-devops`
|
||||
- General k8s security posture → use `cloud-security`
|
||||
|
||||
## Core principle: an operator is a reconcile loop
|
||||
|
||||
```
|
||||
observe(actual) → desired = read(spec) → diff(actual, desired) → act → update(status)
|
||||
↓
|
||||
requeue / done
|
||||
```
|
||||
|
||||
## The 3 Python tools
|
||||
|
||||
All stdlib-only.
|
||||
|
||||
### `crd_validator.py`
|
||||
|
||||
Validates a CRD YAML against operator-pattern best practices: status subresource, structural schema, conditions array, printer columns, version policy.
|
||||
|
||||
```bash
|
||||
python scripts/crd_validator.py --crd config/crd/myapp.yaml
|
||||
```
|
||||
|
||||
### `reconcile_lint.py`
|
||||
|
||||
Lints Go reconcile functions for anti-patterns: `time.Sleep` (blocks queue), spec mutation (should be status), missing requeue on errors, oversized reconcile functions, finalizer add without remove.
|
||||
|
||||
```bash
|
||||
python scripts/reconcile_lint.py --controller controllers/myapp_controller.go
|
||||
```
|
||||
|
||||
### `operator_capability_audit.py`
|
||||
|
||||
Scores against OperatorHub Capability Levels (1-5):
|
||||
- **L1** Basic Install — CRD + controller + Deployment
|
||||
- **L2** Seamless Upgrades — conversion webhook + PDB + leader election
|
||||
- **L3** Full Lifecycle — finalizers + status conditions + backup/restore
|
||||
- **L4** Deep Insights — metrics + Prometheus rules
|
||||
- **L5** Auto Pilot — autoscaling + autotuning + anomaly detection
|
||||
|
||||
```bash
|
||||
python scripts/operator_capability_audit.py --operator-dir .
|
||||
```
|
||||
|
||||
Reports current level + concrete next-level advancement steps.
|
||||
|
||||
## Framework chooser
|
||||
|
||||
| Framework | Language | Best for |
|
||||
|---|---|---|
|
||||
| **controller-runtime** | Go | Library-only, full control |
|
||||
| **kubebuilder** | Go | Standard Go scaffolding |
|
||||
| **operator-sdk** | Go / Helm / Ansible | OpenShift / OLM / mixed paradigm |
|
||||
| **metacontroller** | Any | Polyglot, webhook-based |
|
||||
| **KOPF** | Python | Python shops, async-first |
|
||||
|
||||
See `references/tooling_landscape.md` for full comparison + decision tree.
|
||||
|
||||
## Asset templates
|
||||
|
||||
- `assets/crd_template.yaml` — production CRD with status subresource, conditions, printer columns (passes `crd_validator.py`)
|
||||
- `assets/reconcile_skeleton.go` — Go controller with idempotency, conditions, finalizers, requeue patterns (passes `reconcile_lint.py`)
|
||||
|
||||
## Slash command
|
||||
|
||||
`/operator-audit` — Run all 3 tools on an operator repo and produce a markdown report.
|
||||
|
||||
## Reference docs
|
||||
|
||||
- `references/operator_pattern.md` — what an operator IS, when to use vs alternatives
|
||||
- `references/crd_design.md` — CRD design principles, versioning, conversion webhooks
|
||||
- `references/reconcile_loop.md` — reconcile patterns, error handling, idempotency
|
||||
- `references/tooling_landscape.md` — framework comparison + decision tree
|
||||
|
||||
## Verifiable success
|
||||
|
||||
A team using this skill should achieve:
|
||||
|
||||
- 100% of new CRDs pass `crd_validator.py` before merge
|
||||
- All reconcile functions pass `reconcile_lint.py` strict mode
|
||||
- Operators reach OperatorHub Capability Level 3 before public release
|
||||
- Mean time to fix a reconcile bug: <1 day (no infinite loops in production)
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "engineering-advanced-skills",
|
||||
"description": "46 advanced engineering skills: agent designer, agent workflow designer, AgentHub, RAG architect, database designer, migration architect, observability designer, dependency auditor, release manager, API reviewer, CI/CD pipeline builder, MCP server builder, skill security auditor, performance profiler, Helm chart builder, Terraform patterns, focused-fix, browser-automation, spec-driven-workflow, secrets-vault-manager, sql-database-assistant, self-eval, llm-cost-optimizer, prompt-governance, llm-wiki (second brain for Obsidian + Claude Code, Karpathy pattern), tc-tracker (task context tracker with lifecycle and handoff format), feature-flags-architect (flag debt scanner, rollout planner, kill-switch audit), and more. Agent skill and plugin for Claude Code, Codex, Gemini CLI, Cursor, OpenClaw.",
|
||||
"version": "2.4.0",
|
||||
"description": "47 advanced engineering skills: agent designer, agent workflow designer, AgentHub, RAG architect, database designer, migration architect, observability designer, dependency auditor, release manager, API reviewer, CI/CD pipeline builder, MCP server builder, skill security auditor, performance profiler, Helm chart builder, Terraform patterns, focused-fix, browser-automation, spec-driven-workflow, secrets-vault-manager, sql-database-assistant, self-eval, llm-cost-optimizer, prompt-governance, llm-wiki (second brain for Obsidian + Claude Code, Karpathy pattern), tc-tracker (task context tracker with lifecycle and handoff format), feature-flags-architect (flag debt scanner, rollout planner, kill-switch audit), kubernetes-operator (CRD validator, reconcile linter, capability auditor), and more. Agent skill and plugin for Claude Code, Codex, Gemini CLI, Cursor, OpenClaw.",
|
||||
"version": "2.4.1",
|
||||
"author": {
|
||||
"name": "Alireza Rezvani",
|
||||
"url": "https://alirezarezvani.com"
|
||||
|
|
|
|||
13
engineering/kubernetes-operator/.claude-plugin/plugin.json
Normal file
13
engineering/kubernetes-operator/.claude-plugin/plugin.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "kubernetes-operator",
|
||||
"description": "End-to-end Kubernetes Operator discipline: CRD design, reconcile-loop patterns, and OperatorHub Capability Levels. Ships CRD validator, reconcile-loop linter, and capability auditor (3 stdlib Python tools), 4 references on the operator pattern + CRD design + reconcile patterns + framework comparison (controller-runtime/kubebuilder/operator-sdk/metacontroller/KOPF), CRD + Go controller skeletons, and /operator-audit slash command. NOT a generic k8s skill — specifically the Operator pattern.",
|
||||
"version": "2.4.0",
|
||||
"author": {
|
||||
"name": "Alireza Rezvani",
|
||||
"url": "https://alirezarezvani.com"
|
||||
},
|
||||
"homepage": "https://github.com/alirezarezvani/claude-skills/tree/main/engineering/kubernetes-operator",
|
||||
"repository": "https://github.com/alirezarezvani/claude-skills",
|
||||
"license": "MIT",
|
||||
"skills": "./skills"
|
||||
}
|
||||
83
engineering/kubernetes-operator/README.md
Normal file
83
engineering/kubernetes-operator/README.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Kubernetes Operator
|
||||
|
||||
End-to-end discipline for building Kubernetes Operators correctly. Catches the recurring reconcile-loop bugs (missing finalizers, blocking calls, status drift, RBAC over-grants, no requeue) before they reach a cluster.
|
||||
|
||||
## What's inside
|
||||
|
||||
- **3 stdlib Python tools** — CRD validator, reconcile-loop linter, OperatorHub capability auditor
|
||||
- **4 reference docs** — operator pattern, CRD design, reconcile patterns, framework comparison
|
||||
- **Asset templates** — production CRD YAML + Go controller skeleton (both pass the linters)
|
||||
- **`/operator-audit` slash command** — runs all 3 tools and produces a report
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
# Via Claude Code marketplace
|
||||
/plugin install kubernetes-operator
|
||||
|
||||
# Or clone the repo
|
||||
git clone https://github.com/alirezarezvani/claude-skills.git
|
||||
cd claude-skills/engineering/kubernetes-operator
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
SKILL=engineering/kubernetes-operator/skills/kubernetes-operator
|
||||
|
||||
python "$SKILL/scripts/crd_validator.py" --crd config/crd/myapp.yaml
|
||||
python "$SKILL/scripts/reconcile_lint.py" --controller controllers/myapp_controller.go
|
||||
python "$SKILL/scripts/operator_capability_audit.py" --operator-dir .
|
||||
```
|
||||
|
||||
## Scope
|
||||
|
||||
This is the **Operator pattern** specifically. For other Kubernetes work:
|
||||
|
||||
- Helm chart authoring → `helm-chart-builder`
|
||||
- Kubectl operations / blue-green deploys → `senior-devops`
|
||||
- General k8s security → `cloud-security`
|
||||
- Cloud architecture → `aws-solution-architect`, `azure-cloud-architect`, `gcp-cloud-architect`
|
||||
|
||||
## Key principles
|
||||
|
||||
1. **Reconcile is idempotent**, declarative, and bounded in time
|
||||
2. **Status subresource is non-negotiable** — without it, status updates loop spec reconciles
|
||||
3. **Finalizers protect external resources** — cascade deletion is the operator pattern's free gift, but only for owned k8s resources
|
||||
4. **RBAC is least-privilege** — controllers shouldn't read secrets they don't need
|
||||
5. **Capability levels are an SLA**, not a label — aim for L3 (Full Lifecycle) before public release
|
||||
|
||||
## Skill structure
|
||||
|
||||
```
|
||||
kubernetes-operator/
|
||||
├── README.md
|
||||
├── .claude-plugin/plugin.json
|
||||
└── skills/kubernetes-operator/
|
||||
├── SKILL.md
|
||||
├── scripts/
|
||||
│ ├── crd_validator.py
|
||||
│ ├── reconcile_lint.py
|
||||
│ └── operator_capability_audit.py
|
||||
├── references/
|
||||
│ ├── operator_pattern.md
|
||||
│ ├── crd_design.md
|
||||
│ ├── reconcile_loop.md
|
||||
│ └── tooling_landscape.md
|
||||
└── assets/
|
||||
├── crd_template.yaml
|
||||
└── reconcile_skeleton.go
|
||||
```
|
||||
|
||||
## Verifiable success
|
||||
|
||||
A team using this skill should achieve:
|
||||
|
||||
- 100% of new CRDs pass `crd_validator.py` before merge
|
||||
- All reconcile functions pass `reconcile_lint.py` strict mode
|
||||
- Operators reach OperatorHub Capability Level 3 before public release
|
||||
- Mean time to fix a reconcile bug: <1 day (no infinite loops in production)
|
||||
|
||||
## License
|
||||
|
||||
MIT — see repo root LICENSE.
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
---
|
||||
name: kubernetes-operator
|
||||
description: Use when building a Kubernetes Operator — custom controllers that reconcile CRD state. Triggers on "build an operator", "CRD design", "reconcile loop", "controller-runtime", "kubebuilder", "operator-sdk", "metacontroller", "KOPF", "operator capability levels", or "custom resource". Ships CRD validator, reconcile-loop linter, and OperatorHub capability auditor (all stdlib Python), 4 references on the operator pattern + CRD design + reconcile patterns + tooling landscape, and a /operator-audit slash command. NOT a generic k8s skill — specifically the Operator pattern.
|
||||
context: fork
|
||||
version: 2.4.0
|
||||
author: claude-code-skills
|
||||
license: MIT
|
||||
tags: [kubernetes, operator, crd, controller-runtime, kubebuilder, operator-sdk, metacontroller, kopf, reconcile, devops]
|
||||
compatible_tools: [claude-code, codex-cli, cursor, antigravity, opencode, gemini-cli]
|
||||
---
|
||||
|
||||
# Kubernetes Operator
|
||||
|
||||
Build operators that reconcile correctly. Most operator bugs are not Kubernetes bugs — they are reconcile-loop bugs: missing finalizers, blocking calls, no requeue on transient errors, status drift, RBAC over-grants. This skill catches them deterministically before they reach a cluster.
|
||||
|
||||
## When to use
|
||||
|
||||
- Building a new Kubernetes Operator (controller for a CRD)
|
||||
- Reviewing an existing operator for capability-level gaps
|
||||
- Auditing a CRD spec for status/conditions/finalizer correctness
|
||||
- Choosing a framework (controller-runtime / kubebuilder / operator-sdk / metacontroller / KOPF)
|
||||
- Designing the API surface of a Custom Resource
|
||||
- Hardening RBAC, leader election, or webhook validation
|
||||
|
||||
## When NOT to use
|
||||
|
||||
- Plain Helm chart packaging → use `helm-chart-builder`
|
||||
- Standard kubectl operations / blue-green deploys → use `senior-devops`
|
||||
- General k8s security posture → use `cloud-security`
|
||||
- "I want to run a workload" — that's a Deployment / Job, not an operator
|
||||
|
||||
## Core principle: an operator is a reconcile loop, not a script
|
||||
|
||||
```
|
||||
observe(actual) → desired = read(spec) → diff(actual, desired) → act → update(status)
|
||||
↓
|
||||
requeue / done
|
||||
```
|
||||
|
||||
Operators that fail are the ones that:
|
||||
1. Treat reconcile as imperative (do this, then this, then this) instead of declarative (make actual=desired, idempotently)
|
||||
2. Don't requeue transient failures
|
||||
3. Don't use finalizers, leaving orphan resources
|
||||
4. Mutate spec instead of status
|
||||
5. Don't use the status subresource (status updates trigger spec reconciles → loop)
|
||||
6. Block in reconcile (long HTTP calls, locks)
|
||||
7. Forget leader election → split-brain on multi-replica deploys
|
||||
|
||||
The 3 tools below catch each of these.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
SKILL=engineering/kubernetes-operator/skills/kubernetes-operator
|
||||
|
||||
# Validate a CRD design
|
||||
python "$SKILL/scripts/crd_validator.py" --crd config/crd/myapp.yaml
|
||||
|
||||
# Lint a Go reconcile function
|
||||
python "$SKILL/scripts/reconcile_lint.py" --controller controllers/myapp_controller.go
|
||||
|
||||
# Score against OperatorHub Capability Levels (1-5)
|
||||
python "$SKILL/scripts/operator_capability_audit.py" --operator-dir .
|
||||
```
|
||||
|
||||
## The 3 Python tools
|
||||
|
||||
All stdlib-only. Run with `--help`.
|
||||
|
||||
### `crd_validator.py`
|
||||
|
||||
Validates a CRD YAML against operator-pattern best practices.
|
||||
|
||||
```bash
|
||||
python scripts/crd_validator.py --crd config/crd/myapp.yaml
|
||||
python scripts/crd_validator.py --crd config/crd/ --format json
|
||||
```
|
||||
|
||||
**Checks:**
|
||||
- `spec.versions[*].subresources.status` is set (status subresource)
|
||||
- `spec.scope` is `Namespaced` (not `Cluster`) unless explicitly justified
|
||||
- Singular and listKind defined
|
||||
- `spec.versions[*].schema.openAPIV3Schema` has type definitions (no `x-kubernetes-preserve-unknown-fields: true` at top level)
|
||||
- A version is marked `served: true` AND `storage: true`
|
||||
- Conditions array is in the schema (allows `metav1.Conditions`)
|
||||
- Printer columns include `Age` and `Status`/`Phase`
|
||||
|
||||
### `reconcile_lint.py`
|
||||
|
||||
Lints a Go controller reconcile function for anti-patterns.
|
||||
|
||||
```bash
|
||||
python scripts/reconcile_lint.py --controller controllers/myapp_controller.go
|
||||
```
|
||||
|
||||
**Checks (regex-based heuristics):**
|
||||
- Returns are `(ctrl.Result, error)` shape
|
||||
- Errors trigger a non-zero requeue (`return ctrl.Result{Requeue: true}, err`)
|
||||
- `client.Update()` on the spec object is flagged (controllers should update only status)
|
||||
- `time.Sleep` inside reconcile is flagged (use `RequeueAfter`)
|
||||
- HTTP calls without context cancellation are flagged
|
||||
- Missing `defer` after a finalizer add
|
||||
- No `IsConditionTrue` / `SetCondition` calls when conditions present in CRD
|
||||
- Reconcile function exceeds 80 lines (extract subroutines)
|
||||
|
||||
### `operator_capability_audit.py`
|
||||
|
||||
Scores an operator against OperatorHub's 5 Capability Levels.
|
||||
|
||||
```bash
|
||||
python scripts/operator_capability_audit.py --operator-dir .
|
||||
```
|
||||
|
||||
**Levels:**
|
||||
- **L1 — Basic Install:** CRD defined, controller deploys it
|
||||
- **L2 — Seamless Upgrades:** PDBs, conversion webhooks, version skew strategy
|
||||
- **L3 — Full Lifecycle:** backups, restores, failure recovery
|
||||
- **L4 — Deep Insights:** metrics endpoint, Prometheus rules, alerts
|
||||
- **L5 — Auto Pilot:** auto-scaling, auto-tuning, anomaly detection
|
||||
|
||||
Reports current level + concrete next steps to advance one level.
|
||||
|
||||
## Tooling landscape
|
||||
|
||||
Pick a framework based on language and complexity. See `references/tooling_landscape.md`.
|
||||
|
||||
| Framework | Language | Best for | Maintenance |
|
||||
|---|---|---|---|
|
||||
| **controller-runtime** | Go | Production-grade, low-level control | Active (sig-api-machinery) |
|
||||
| **kubebuilder** | Go | Standard scaffolding, opinionated | Active (Kubernetes SIGs) |
|
||||
| **operator-sdk** | Go / Helm / Ansible | OpenShift / mixed-paradigm teams | Active (Red Hat) |
|
||||
| **metacontroller** | Any (webhook-based) | Polyglot teams, avoiding Go | Less active |
|
||||
| **KOPF** | Python | Python shops, async-first | Active (community) |
|
||||
| **java-operator-sdk** | Java | JVM shops | Active (Red Hat / Java SIG) |
|
||||
|
||||
Decision rules:
|
||||
- New operator + Go shop → kubebuilder
|
||||
- New operator + Python shop → KOPF
|
||||
- New operator + can't pick a language → metacontroller
|
||||
- OpenShift target → operator-sdk
|
||||
|
||||
## CRD design principles
|
||||
|
||||
See `references/crd_design.md` for full detail. Quick rules:
|
||||
|
||||
1. **status is the source of truth for the controller's view of the world.** Spec is what the user wants; status is what the controller observed.
|
||||
2. **Use the status subresource.** Without it, status updates re-trigger reconcile (loop).
|
||||
3. **Use Conditions.** `Ready`, `Reconciling`, `Degraded`. Each carries a reason and message.
|
||||
4. **Add finalizers.** Without finalizers, deletion races the controller and orphans external resources.
|
||||
5. **Version your CRD from day 1.** `v1alpha1` → `v1beta1` → `v1`. Plan a conversion webhook.
|
||||
6. **Validate via OpenAPI v3 schema.** Don't rely on the controller for validation that should fail at admission.
|
||||
7. **Use `additionalPrinterColumns` for `kubectl get`.** Show `Age`, `Phase`, `Ready` at minimum.
|
||||
8. **Namespace your CRDs unless they manage cluster-scoped resources.**
|
||||
|
||||
## Reconcile loop principles
|
||||
|
||||
See `references/reconcile_loop.md` for full detail. Quick rules:
|
||||
|
||||
1. **Idempotent.** Reconciling the same state twice → same result, zero side effects.
|
||||
2. **Read once, decide, act.** Don't observe the world repeatedly during reconcile.
|
||||
3. **Update status, not spec.** Spec belongs to the user.
|
||||
4. **Return errors that requeue.** Use `ctrl.Result{RequeueAfter: ...}` for known transient cases.
|
||||
5. **Never block.** No `time.Sleep`. No long HTTP calls without context.
|
||||
6. **Use the cache.** Read via the controller's cached client; only escape the cache for a specific reason.
|
||||
7. **Leader-elect when running >1 replica.** Otherwise enable single-replica mode.
|
||||
8. **Set OwnerReferences.** Cascading deletion is the operator pattern's free gift.
|
||||
|
||||
## Workflows
|
||||
|
||||
### Workflow 1: Bootstrap a new operator (Go + kubebuilder)
|
||||
|
||||
```
|
||||
1. Pick a Group/Version/Kind: e.g., apps.example.com/v1alpha1, kind=MyApp
|
||||
2. kubebuilder init --domain example.com --repo github.com/org/myapp-operator
|
||||
3. kubebuilder create api --group apps --version v1alpha1 --kind MyApp
|
||||
4. Run crd_validator.py on config/crd/bases/apps.example.com_myapps.yaml
|
||||
→ Fix every WARN before writing controller code
|
||||
5. Implement the reconcile function (Karpathy principle 2: simplest correct version first)
|
||||
6. Run reconcile_lint.py on controllers/myapp_controller.go
|
||||
7. Run operator_capability_audit.py --operator-dir . — confirm L1
|
||||
8. Test in a kind cluster: kubectl apply -f config/samples/
|
||||
9. Add status conditions; aim for L2 in the same PR
|
||||
```
|
||||
|
||||
### Workflow 2: Audit an existing operator
|
||||
|
||||
```
|
||||
1. Run operator_capability_audit.py --operator-dir <path>
|
||||
2. Run crd_validator.py --crd config/crd/
|
||||
3. Run reconcile_lint.py --controller controllers/
|
||||
4. Triage findings:
|
||||
- FAIL → block release; fix before next deploy
|
||||
- WARN → file an issue; fix in next 30 days
|
||||
5. Document current capability level in README; commit
|
||||
6. Plan one capability level advancement per quarter
|
||||
```
|
||||
|
||||
### Workflow 3: Choose a framework
|
||||
|
||||
```
|
||||
1. Identify primary language constraint (team skill)
|
||||
2. Identify deployment target (vanilla k8s vs OpenShift)
|
||||
3. Identify operator complexity (single CRD vs multi-CRD vs cluster-wide)
|
||||
4. Cross-reference with references/tooling_landscape.md
|
||||
5. Build a 1-week proof-of-concept before committing
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- `references/operator_pattern.md` — what an operator IS, when to use vs alternatives
|
||||
- `references/crd_design.md` — CRD design principles, versioning, conversion webhooks
|
||||
- `references/reconcile_loop.md` — reconcile patterns, error handling, idempotency
|
||||
- `references/tooling_landscape.md` — framework comparison + decision tree
|
||||
|
||||
## Slash command
|
||||
|
||||
`/operator-audit` — Run all 3 tools on an operator repo and produce a markdown report.
|
||||
|
||||
## Asset templates
|
||||
|
||||
- `assets/crd_template.yaml` — CRD with status subresource, conditions, finalizer hint, printer columns
|
||||
- `assets/reconcile_skeleton.go` — Go controller reconcile function with idempotency, conditions, finalizers, requeue patterns
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- **`time.Sleep(30 * time.Second)` inside reconcile** — block other reconciles. Use `RequeueAfter`.
|
||||
- **`r.Client.Update(ctx, obj)` to set status** — use `r.Status().Update(ctx, obj)` instead.
|
||||
- **No leader election + 2+ replicas** — split-brain.
|
||||
- **No finalizer** — external resources orphan on deletion.
|
||||
- **CRD without status subresource** — status updates trigger spec reconciles (infinite loop).
|
||||
- **Reconcile function > 200 lines** — extract reconcileXxx subroutines per condition.
|
||||
- **`x-kubernetes-preserve-unknown-fields: true` on spec root** — defeats validation.
|
||||
- **Imperative reconcile** — "if creating, do A; if updating, do B; if deleting, do C". Wrong shape. Reconcile = make actual=desired, regardless of how we got here.
|
||||
|
||||
## Verifiable success
|
||||
|
||||
A team using this skill should achieve:
|
||||
|
||||
- 100% of new CRDs pass `crd_validator.py` before merge
|
||||
- All reconcile functions pass `reconcile_lint.py` strict mode
|
||||
- Operators reach OperatorHub Capability Level 3 (Full Lifecycle) before public release
|
||||
- Mean time to fix a reconcile bug: <1 day (no infinite loops in production)
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
# Production CRD template — passes crd_validator.py
|
||||
# Fill in <PLACEHOLDERS>; remove these comments before applying.
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: <plural>.<group> # e.g., myapps.apps.example.com
|
||||
spec:
|
||||
group: <group> # e.g., apps.example.com
|
||||
names:
|
||||
kind: <Kind> # e.g., MyApp
|
||||
plural: <plural> # e.g., myapps
|
||||
singular: <singular> # e.g., myapp
|
||||
listKind: <Kind>List # e.g., MyAppList
|
||||
shortNames: [<short>] # optional, 2-3 letters
|
||||
scope: Namespaced # default; Cluster requires justification
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
storage: true
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
required: [version]
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
pattern: '^[0-9]+\.[0-9]+\.[0-9]+$'
|
||||
description: Semver version of the application
|
||||
replicas:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 3
|
||||
description: Number of replicas to run
|
||||
status:
|
||||
type: object
|
||||
properties:
|
||||
phase:
|
||||
type: string
|
||||
enum: [Pending, Running, Failed]
|
||||
observedGeneration:
|
||||
type: integer
|
||||
description: Spec generation last reconciled
|
||||
conditions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [type, status, lastTransitionTime]
|
||||
properties:
|
||||
type: { type: string }
|
||||
status: { type: string, enum: ["True", "False", "Unknown"] }
|
||||
reason: { type: string }
|
||||
message: { type: string }
|
||||
lastTransitionTime: { type: string, format: date-time }
|
||||
observedGeneration: { type: integer }
|
||||
subresources:
|
||||
status: {} # CRITICAL — enables /status subresource
|
||||
additionalPrinterColumns:
|
||||
- name: Phase
|
||||
type: string
|
||||
jsonPath: .status.phase
|
||||
- name: Ready
|
||||
type: string
|
||||
jsonPath: .status.conditions[?(@.type=="Ready")].status
|
||||
- name: Age
|
||||
type: date
|
||||
jsonPath: .metadata.creationTimestamp
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
// Reconcile skeleton — passes reconcile_lint.py.
|
||||
// Replace <PLACEHOLDER> markers; rename receiver + types to match your CR.
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
|
||||
appsv1alpha1 "<MODULE>/api/v1alpha1"
|
||||
)
|
||||
|
||||
const finalizerName = "<group>/finalizer"
|
||||
|
||||
type MyAppReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx).WithValues("myapp", req.NamespacedName)
|
||||
|
||||
var cr appsv1alpha1.MyApp
|
||||
if err := r.Get(ctx, req.NamespacedName, &cr); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if !cr.DeletionTimestamp.IsZero() {
|
||||
return r.reconcileDelete(ctx, &cr)
|
||||
}
|
||||
|
||||
if !controllerutil.ContainsFinalizer(&cr, finalizerName) {
|
||||
controllerutil.AddFinalizer(&cr, finalizerName)
|
||||
return ctrl.Result{}, r.Update(ctx, &cr)
|
||||
}
|
||||
|
||||
meta.SetStatusCondition(&cr.Status.Conditions, metav1.Condition{
|
||||
Type: "Reconciling",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "InProgress",
|
||||
Message: "Converging to desired state",
|
||||
ObservedGeneration: cr.Generation,
|
||||
})
|
||||
|
||||
res, recErr := r.reconcileNormal(ctx, &cr)
|
||||
|
||||
if recErr == nil {
|
||||
meta.SetStatusCondition(&cr.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready", Status: metav1.ConditionTrue,
|
||||
Reason: "AllReady", Message: "all components healthy",
|
||||
ObservedGeneration: cr.Generation,
|
||||
})
|
||||
} else {
|
||||
meta.SetStatusCondition(&cr.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready", Status: metav1.ConditionFalse,
|
||||
Reason: "ReconcileError", Message: recErr.Error(),
|
||||
ObservedGeneration: cr.Generation,
|
||||
})
|
||||
}
|
||||
|
||||
cr.Status.ObservedGeneration = cr.Generation
|
||||
|
||||
if statusErr := r.Status().Update(ctx, &cr); statusErr != nil {
|
||||
logger.Error(statusErr, "failed to update status")
|
||||
return res, errors.Join(recErr, statusErr)
|
||||
}
|
||||
return res, recErr
|
||||
}
|
||||
|
||||
func (r *MyAppReconciler) reconcileNormal(ctx context.Context, cr *appsv1alpha1.MyApp) (ctrl.Result, error) {
|
||||
// Idempotent: read desired, build child, CreateOrUpdate.
|
||||
deployment := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: cr.Name, Namespace: cr.Namespace}}
|
||||
op, err := controllerutil.CreateOrUpdate(ctx, r.Client, deployment, func() error {
|
||||
deployment.Spec.Replicas = &cr.Spec.Replicas
|
||||
// Build container spec from cr.Spec — extracted helper for clarity
|
||||
// deployment.Spec.Template.Spec.Containers = buildContainers(&cr.Spec)
|
||||
return controllerutil.SetControllerReference(cr, deployment, r.Scheme)
|
||||
})
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
log.FromContext(ctx).Info("deployment", "operation", op)
|
||||
|
||||
// Periodic resync — keeps status fresh even when nothing changes.
|
||||
return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
|
||||
}
|
||||
|
||||
func (r *MyAppReconciler) reconcileDelete(ctx context.Context, cr *appsv1alpha1.MyApp) (ctrl.Result, error) {
|
||||
if !controllerutil.ContainsFinalizer(cr, finalizerName) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
if err := r.deleteExternalResources(ctx, cr); err != nil {
|
||||
return ctrl.Result{RequeueAfter: 30 * time.Second}, err
|
||||
}
|
||||
controllerutil.RemoveFinalizer(cr, finalizerName)
|
||||
return ctrl.Result{}, r.Update(ctx, cr)
|
||||
}
|
||||
|
||||
func (r *MyAppReconciler) deleteExternalResources(ctx context.Context, cr *appsv1alpha1.MyApp) error {
|
||||
// Implement teardown of external state (cloud DB, S3 bucket, DNS record, ...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *MyAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&appsv1alpha1.MyApp{}).
|
||||
Owns(&appsv1.Deployment{}).
|
||||
WithEventFilter(predicate.GenerationChangedPredicate{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
# CRD design
|
||||
|
||||
Custom Resource Definitions (CRDs) define the API surface of your operator. A bad CRD design locks you into hard-to-evolve schemas, forces wrapper APIs, and creates user-facing UX problems via `kubectl`.
|
||||
|
||||
## Anatomy of a production CRD
|
||||
|
||||
```yaml
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: myapps.apps.example.com # plural.group
|
||||
spec:
|
||||
group: apps.example.com
|
||||
names:
|
||||
kind: MyApp # PascalCase
|
||||
plural: myapps # lowercase
|
||||
singular: myapp # lowercase
|
||||
listKind: MyAppList # KindList
|
||||
shortNames: [ma] # optional
|
||||
scope: Namespaced # or Cluster (justify)
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
storage: true
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
required: [version]
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
pattern: '^[0-9]+\.[0-9]+\.[0-9]+$'
|
||||
replicas:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 3
|
||||
status:
|
||||
type: object
|
||||
properties:
|
||||
phase:
|
||||
type: string
|
||||
enum: [Pending, Running, Failed]
|
||||
conditions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [type, status, lastTransitionTime]
|
||||
properties:
|
||||
type: { type: string }
|
||||
status: { type: string, enum: ["True", "False", "Unknown"] }
|
||||
reason: { type: string }
|
||||
message: { type: string }
|
||||
lastTransitionTime: { type: string, format: date-time }
|
||||
observedGeneration: { type: integer }
|
||||
subresources:
|
||||
status: {} # CRITICAL — see below
|
||||
scale: # if scaling is meaningful
|
||||
specReplicasPath: .spec.replicas
|
||||
statusReplicasPath: .status.readyReplicas
|
||||
additionalPrinterColumns:
|
||||
- name: Phase
|
||||
type: string
|
||||
jsonPath: .status.phase
|
||||
- name: Ready
|
||||
type: string
|
||||
jsonPath: .status.conditions[?(@.type=="Ready")].status
|
||||
- name: Age
|
||||
type: date
|
||||
jsonPath: .metadata.creationTimestamp
|
||||
```
|
||||
|
||||
## Required structural elements
|
||||
|
||||
### 1. Status subresource — `subresources.status: {}`
|
||||
|
||||
Without it:
|
||||
- `r.Status().Update(ctx, obj)` doesn't work — falls back to `r.Update`
|
||||
- Status updates re-trigger spec reconcile → loop
|
||||
- RBAC can't be split between spec writers and status writers
|
||||
|
||||
**Always declare it.**
|
||||
|
||||
### 2. Conditions array
|
||||
|
||||
Use the standard `metav1.Condition` shape. Required fields: `type`, `status`, `lastTransitionTime`. Recommended: `reason`, `message`, `observedGeneration`.
|
||||
|
||||
Conventional condition types:
|
||||
- `Ready` — overall readiness
|
||||
- `Reconciling` — controller is actively working
|
||||
- `Degraded` — operating but with reduced capability
|
||||
- `Progressing` — change in progress (mostly for Deployments-style flows)
|
||||
|
||||
Use `meta.SetStatusCondition()` from `k8s.io/apimachinery/pkg/api/meta` — don't write to the slice directly.
|
||||
|
||||
### 3. observedGeneration
|
||||
|
||||
Track which spec generation the controller has acted on:
|
||||
|
||||
```go
|
||||
status.ObservedGeneration = obj.Generation
|
||||
```
|
||||
|
||||
Lets users tell whether status reflects the latest spec or a previous one.
|
||||
|
||||
### 4. Printer columns
|
||||
|
||||
`kubectl get myapp` UX is determined by `additionalPrinterColumns`. Always include:
|
||||
- `Phase` or `Ready` (status)
|
||||
- `Age` (so users know when it was created)
|
||||
|
||||
Optionally: replicas, version, key spec field.
|
||||
|
||||
### 5. Validation in the schema, not the controller
|
||||
|
||||
Express constraints declaratively:
|
||||
|
||||
| Constraint | OpenAPI |
|
||||
|---|---|
|
||||
| Range | `minimum`/`maximum` |
|
||||
| String pattern | `pattern: '^...$'` |
|
||||
| Enum | `enum: [Pending, Running]` |
|
||||
| Required field | `required: [...]` |
|
||||
| Default value | `default: 3` |
|
||||
| Min/max length | `minLength`/`maxLength` |
|
||||
|
||||
Reserve controller validation for cross-field rules and external dependencies (e.g., "this name is taken in our DB").
|
||||
|
||||
### 6. Avoid `x-kubernetes-preserve-unknown-fields: true`
|
||||
|
||||
It disables structural validation. Sometimes needed (e.g., raw `kubectl apply` patches), but never at the spec root. Use it sparingly on a single sub-tree.
|
||||
|
||||
## Versioning strategy
|
||||
|
||||
CRDs evolve. Plan from day 1:
|
||||
|
||||
| Stage | Version | Stability | Allowed changes |
|
||||
|---|---|---|---|
|
||||
| Internal preview | `v1alpha1` | None | Anything; document breaking changes |
|
||||
| Beta | `v1beta1` | Some | Additive only; deprecate fields |
|
||||
| GA | `v1` | Strong | Additive only; never remove fields |
|
||||
|
||||
Conversion webhook required when:
|
||||
- Multiple versions are served simultaneously
|
||||
- A field's shape changed between versions
|
||||
|
||||
For simple field renames, `x-kubernetes-conversion-strategy: None` works.
|
||||
|
||||
## Scope: Namespaced vs Cluster
|
||||
|
||||
Default to **Namespaced**. Cluster-scoped CRDs:
|
||||
- Can't be RBAC-restricted by namespace
|
||||
- Can't have `OwnerReferences` from namespaced parents
|
||||
- Are appropriate only for cluster-wide resources (`StorageClass`-like things)
|
||||
|
||||
If your operator manages namespace-bound things (apps, databases, queues), use Namespaced.
|
||||
|
||||
## Naming
|
||||
|
||||
- **Group**: `<domain>.<reverse-domain>` — e.g., `apps.example.com`. Don't use generic groups (`com`, `io`).
|
||||
- **Kind**: PascalCase, singular, descriptive — `MyApp`, `Database`, `Cache`. Avoid `MyAppResource` (the `Resource` suffix is implicit).
|
||||
- **Plural**: lowercase, plural — `myapps`, `databases`, `caches`.
|
||||
- **Short name**: 2-3 letters; check for conflicts with built-in resources.
|
||||
|
||||
## Validation tooling
|
||||
|
||||
- `kubectl apply --dry-run=server` — validates against your CRD
|
||||
- `kubectl explain <kind>.<field>` — shows what your schema documents
|
||||
- `crd_validator.py` — this skill's tool, structural rules
|
||||
|
||||
## Documentation in the schema
|
||||
|
||||
Use the `description` field on every property. `kubectl explain` reads it:
|
||||
|
||||
```yaml
|
||||
properties:
|
||||
replicas:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: |
|
||||
Number of replicas to run. Production deployments should use ≥3.
|
||||
Increases above 100 require quota approval.
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- **Top-level `x-kubernetes-preserve-unknown-fields: true`** — defeats validation
|
||||
- **No `scope:` declared** — defaults to namespaced but make intent explicit
|
||||
- **No printer columns** — `kubectl get` shows only `NAME AGE`
|
||||
- **Conditions written by hand** (not via `SetStatusCondition`) — easy to lose `lastTransitionTime`
|
||||
- **Status fields that duplicate spec** — keep them separate
|
||||
- **Using `metadata.annotations` to encode operator state** — use status fields
|
||||
- **Single huge CRD with 50+ fields** — split into multiple CRDs (e.g., MyApp + MyAppBackup + MyAppRestore)
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
# 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:
|
||||
|
||||
```yaml
|
||||
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.
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
# The reconcile loop
|
||||
|
||||
Reconcile is the heart of an operator. Most operator bugs are reconcile-loop bugs. The patterns below are deterministic — copy them.
|
||||
|
||||
## Skeleton — `Reconcile(ctx, req)`
|
||||
|
||||
```go
|
||||
func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
log := log.FromContext(ctx)
|
||||
|
||||
// 1. Fetch the CR
|
||||
var cr appsv1alpha1.MyApp
|
||||
if err := r.Get(ctx, req.NamespacedName, &cr); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return ctrl.Result{}, nil // CR is gone; nothing to do
|
||||
}
|
||||
return ctrl.Result{}, err // transient error → requeue
|
||||
}
|
||||
|
||||
// 2. Handle deletion via finalizer
|
||||
if !cr.DeletionTimestamp.IsZero() {
|
||||
return r.reconcileDelete(ctx, &cr)
|
||||
}
|
||||
if !controllerutil.ContainsFinalizer(&cr, finalizerName) {
|
||||
controllerutil.AddFinalizer(&cr, finalizerName)
|
||||
return ctrl.Result{}, r.Update(ctx, &cr)
|
||||
}
|
||||
|
||||
// 3. Mark Reconciling
|
||||
meta.SetStatusCondition(&cr.Status.Conditions, metav1.Condition{
|
||||
Type: "Reconciling", Status: metav1.ConditionTrue,
|
||||
Reason: "InProgress", Message: "Converging to desired state",
|
||||
ObservedGeneration: cr.Generation,
|
||||
})
|
||||
|
||||
// 4. Do the work, idempotently
|
||||
res, err := r.reconcileNormal(ctx, &cr)
|
||||
|
||||
// 5. Update status (always — even on error)
|
||||
if statusErr := r.Status().Update(ctx, &cr); statusErr != nil {
|
||||
log.Error(statusErr, "failed to update status")
|
||||
return res, errors.Join(err, statusErr)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
```
|
||||
|
||||
## The 5-step shape
|
||||
|
||||
1. **Fetch the CR.** Handle `NotFound` cleanly — the CR may have been deleted between event and reconcile.
|
||||
2. **Handle deletion.** If `DeletionTimestamp` is set, run cleanup, remove finalizer, return.
|
||||
3. **Set Reconciling condition.** Mark that the controller is working.
|
||||
4. **Do work idempotently.** Use `CreateOrUpdate`, compare desired-vs-actual, only act on differences.
|
||||
5. **Update status.** Even on error — partial progress is signal.
|
||||
|
||||
## Idempotence patterns
|
||||
|
||||
### Pattern: CreateOrUpdate
|
||||
|
||||
```go
|
||||
deployment := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: cr.Name, Namespace: cr.Namespace}}
|
||||
op, err := controllerutil.CreateOrUpdate(ctx, r.Client, deployment, func() error {
|
||||
deployment.Spec.Replicas = &cr.Spec.Replicas
|
||||
deployment.Spec.Template.Spec.Containers = buildContainers(&cr.Spec)
|
||||
return controllerutil.SetControllerReference(&cr, deployment, r.Scheme)
|
||||
})
|
||||
if err != nil { return ctrl.Result{}, err }
|
||||
log.Info("deployment", "operation", op) // "created", "updated", or "unchanged"
|
||||
```
|
||||
|
||||
This pattern is idempotent by construction.
|
||||
|
||||
### Pattern: SetControllerReference
|
||||
|
||||
Always set the OwnerReference so cascading deletion works:
|
||||
|
||||
```go
|
||||
controllerutil.SetControllerReference(&cr, child, r.Scheme)
|
||||
```
|
||||
|
||||
### Pattern: Finalizer for external resources
|
||||
|
||||
```go
|
||||
const finalizerName = "myapp.apps.example.com/finalizer"
|
||||
|
||||
func (r *MyAppReconciler) reconcileDelete(ctx context.Context, cr *appsv1alpha1.MyApp) (ctrl.Result, error) {
|
||||
if !controllerutil.ContainsFinalizer(cr, finalizerName) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
if err := r.deleteExternalResources(ctx, cr); err != nil {
|
||||
return ctrl.Result{RequeueAfter: 30 * time.Second}, err
|
||||
}
|
||||
controllerutil.RemoveFinalizer(cr, finalizerName)
|
||||
return ctrl.Result{}, r.Update(ctx, cr)
|
||||
}
|
||||
```
|
||||
|
||||
## Error handling and requeue
|
||||
|
||||
| Situation | Return |
|
||||
|---|---|
|
||||
| Permanent error (bad spec) | `ctrl.Result{}, nil` + condition with reason |
|
||||
| Transient error (API timeout, throttling) | `ctrl.Result{}, err` (auto-requeue with backoff) |
|
||||
| Need a retry in N seconds | `ctrl.Result{RequeueAfter: 30*time.Second}, nil` |
|
||||
| Done; no follow-up | `ctrl.Result{}, nil` |
|
||||
|
||||
**Don't use `time.Sleep` inside reconcile.** It blocks the work queue, starving other reconciles. Use `RequeueAfter`.
|
||||
|
||||
## Status update patterns
|
||||
|
||||
```go
|
||||
// Set a condition
|
||||
meta.SetStatusCondition(&cr.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready", Status: metav1.ConditionTrue,
|
||||
Reason: "AllReady", Message: "all components healthy",
|
||||
ObservedGeneration: cr.Generation,
|
||||
})
|
||||
|
||||
// Track observed generation
|
||||
cr.Status.ObservedGeneration = cr.Generation
|
||||
|
||||
// Update status — uses /status subresource
|
||||
if err := r.Status().Update(ctx, &cr); err != nil { ... }
|
||||
```
|
||||
|
||||
**Never** call `r.Update(ctx, &cr)` to update status. It uses the spec subresource, which the user owns.
|
||||
|
||||
## Read once, decide, act
|
||||
|
||||
Don't observe the world repeatedly during reconcile. The cache is read-only and consistent within a single reconcile pass:
|
||||
|
||||
```go
|
||||
// Good: read once, decide, act
|
||||
var pods corev1.PodList
|
||||
r.List(ctx, &pods, client.InNamespace(cr.Namespace), client.MatchingLabels{"app": cr.Name})
|
||||
desired := computeDesired(&cr, &pods)
|
||||
applyDesired(ctx, r.Client, desired)
|
||||
|
||||
// Bad: observe-act-observe-act
|
||||
for _, container := range cr.Spec.Containers {
|
||||
pod := r.Get(...) // re-reading the cache
|
||||
if needsRestart(pod) {
|
||||
r.Delete(...)
|
||||
pod = r.Get(...) // again
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Predicates — filter events you don't care about
|
||||
|
||||
```go
|
||||
func (r *MyAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&appsv1alpha1.MyApp{}).
|
||||
Owns(&appsv1.Deployment{}).
|
||||
WithEventFilter(predicate.GenerationChangedPredicate{}). // ignore status-only updates
|
||||
Complete(r)
|
||||
}
|
||||
```
|
||||
|
||||
`GenerationChangedPredicate` skips reconciles when only status changed — important to avoid loops.
|
||||
|
||||
## Leader election
|
||||
|
||||
Always enable leader election when running >1 controller replica:
|
||||
|
||||
```go
|
||||
mgr, _ := manager.New(cfg, manager.Options{
|
||||
LeaderElection: true,
|
||||
LeaderElectionID: "myapp-operator-leader",
|
||||
})
|
||||
```
|
||||
|
||||
Without it: split-brain. Two controllers both think they own the resource and fight.
|
||||
|
||||
## Performance — bounded reconcile time
|
||||
|
||||
A reconcile pass should complete in <30s for typical work, <2min for heavy work. Longer = the work queue starves other reconciles.
|
||||
|
||||
If work takes longer:
|
||||
- Break into phases; emit `RequeueAfter` between them
|
||||
- Move long-running work to a separate process (Job)
|
||||
- Cache expensive computations on `cr.Status`
|
||||
|
||||
## Logging conventions
|
||||
|
||||
```go
|
||||
log := log.FromContext(ctx).WithValues("phase", "create-deployment")
|
||||
log.Info("creating deployment", "name", cr.Name)
|
||||
log.Error(err, "failed to create deployment")
|
||||
```
|
||||
|
||||
- Use `log.FromContext(ctx)` — picks up controller-runtime's contextual logger
|
||||
- Use `Info` for normal flow, `Error` for retryable failures
|
||||
- Add structured fields, not formatted strings
|
||||
|
||||
## Anti-patterns checklist
|
||||
|
||||
- `time.Sleep` inside reconcile → starves queue; use `RequeueAfter`
|
||||
- `os.Exit` / `log.Fatal` → kills the controller; return an error
|
||||
- `panic` → same; return an error
|
||||
- `r.Update` to set status → use `r.Status().Update`
|
||||
- `r.Update` of the CR while the user could be editing it → use `r.Status().Update` or use Patch
|
||||
- Reading the same resource multiple times in one reconcile → read once
|
||||
- Reconcile body > 80 lines → extract `reconcileXxx` subroutines per phase
|
||||
- HTTP calls without `ctx` → can't cancel during shutdown
|
||||
- No requeue path for transient errors → silent failures
|
||||
- Missing `OwnerReferences` on children → cascading deletion broken
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
# Tooling landscape
|
||||
|
||||
Five mainstream operator frameworks. Pick by language, complexity, and target environment.
|
||||
|
||||
## At-a-glance
|
||||
|
||||
| Framework | Language | Scaffolding | Webhook support | Best for | Project status |
|
||||
|---|---|---|---|---|---|
|
||||
| **controller-runtime** | Go | None (library) | Yes | Production-grade, low-level | Active (sig-api-machinery) |
|
||||
| **kubebuilder** | Go | Yes (CLI) | Yes | Standard Go operator path | Active (Kubernetes SIGs) |
|
||||
| **operator-sdk** | Go / Helm / Ansible | Yes (CLI) | Yes | OpenShift, mixed paradigms | Active (Red Hat) |
|
||||
| **metacontroller** | Any (webhook) | None | N/A (uses webhooks) | Polyglot, avoid Go | Less active |
|
||||
| **KOPF** | Python | None (library) | Yes | Python shops, async-first | Active (community) |
|
||||
| **java-operator-sdk** | Java | Yes | Yes | JVM shops | Active (Red Hat / Java SIG) |
|
||||
|
||||
## Decision tree
|
||||
|
||||
```
|
||||
Primary language?
|
||||
├── Go ──┬── Need scaffolding + opinionated path → kubebuilder
|
||||
│ ├── Targeting OpenShift / OLM → operator-sdk (Go)
|
||||
│ └── Library-only, full control → controller-runtime
|
||||
├── Python ─────────────────────────────────────────→ KOPF
|
||||
├── Java ─────────────────────────────────────────→ java-operator-sdk
|
||||
└── Other (Node, Ruby, Rust)
|
||||
└── webhook-based, polyglot → metacontroller
|
||||
```
|
||||
|
||||
## controller-runtime (Go library)
|
||||
|
||||
**What it is:** The Go library that everyone else builds on. Provides `Manager`, `Reconciler`, cache, client, predicates, leader election.
|
||||
|
||||
**Use when:**
|
||||
- You need fine-grained control over the manager and event sources
|
||||
- You're building reusable operator components
|
||||
- Your team has Go experience and prefers libraries to scaffolders
|
||||
|
||||
**Skip when:**
|
||||
- You want bootstrap-by-CLI (use kubebuilder)
|
||||
- You don't speak Go
|
||||
|
||||
**Example:**
|
||||
```go
|
||||
mgr, _ := ctrl.NewManager(cfg, ctrl.Options{Scheme: scheme})
|
||||
ctrl.NewControllerManagedBy(mgr).
|
||||
For(&apps.MyApp{}).
|
||||
Complete(&MyAppReconciler{Client: mgr.GetClient()})
|
||||
mgr.Start(ctx)
|
||||
```
|
||||
|
||||
## kubebuilder (Go scaffolder)
|
||||
|
||||
**What it is:** The standard scaffolding tool. Wraps controller-runtime with project layout, code generation, and the `kubebuilder` CLI.
|
||||
|
||||
**Use when:**
|
||||
- New Go operator
|
||||
- You want predictable project structure
|
||||
- You'll publish the operator publicly
|
||||
|
||||
**Workflow:**
|
||||
```bash
|
||||
kubebuilder init --domain example.com --repo github.com/org/myapp-operator
|
||||
kubebuilder create api --group apps --version v1alpha1 --kind MyApp
|
||||
make manifests
|
||||
make generate
|
||||
make run
|
||||
```
|
||||
|
||||
**Strengths:** Excellent docs, mature, used by everyone from cert-manager to Crossplane.
|
||||
|
||||
**Weaknesses:** Some teams find the layout opinionated; sometimes hard to escape from.
|
||||
|
||||
## operator-sdk (Red Hat / OpenShift)
|
||||
|
||||
**What it is:** Wraps kubebuilder for Go and adds Helm-based and Ansible-based operators (no Go required).
|
||||
|
||||
**Use when:**
|
||||
- Targeting OpenShift / OLM (Operator Lifecycle Manager)
|
||||
- Building a Helm-based operator from an existing chart
|
||||
- Building an Ansible-based operator from existing playbooks
|
||||
|
||||
**Helm-based operator:**
|
||||
```bash
|
||||
operator-sdk init --plugins=helm --domain example.com --group apps --version v1 --kind MyApp
|
||||
operator-sdk create api --group apps --version v1 --kind MyApp --helm-chart=./mychart
|
||||
```
|
||||
|
||||
The operator's reconcile becomes `helm upgrade --install`. Fast on-ramp; less power.
|
||||
|
||||
**Ansible-based operator:**
|
||||
Similar, but reconcile invokes a playbook. Useful for ops teams already deep in Ansible.
|
||||
|
||||
**Skip when:**
|
||||
- Vanilla k8s target (kubebuilder is more direct)
|
||||
- You want a Go operator without OpenShift coupling
|
||||
|
||||
## metacontroller (webhook-based, language-agnostic)
|
||||
|
||||
**What it is:** Runs in-cluster, watches your CRDs, and POSTs webhook calls to your endpoints with desired-state computations. You implement the logic in any language behind an HTTP endpoint.
|
||||
|
||||
**Use when:**
|
||||
- Polyglot team (Python, Node, Ruby, etc.)
|
||||
- Want to avoid Go and Java
|
||||
- Operator logic is genuinely simple (compute children from parent)
|
||||
|
||||
**Example sync hook:**
|
||||
```python
|
||||
# Python webhook returns desired children given parent + observed
|
||||
def sync(request):
|
||||
parent = request['parent']
|
||||
return {
|
||||
'status': {'phase': 'Ready'},
|
||||
'children': [{'apiVersion': 'apps/v1', 'kind': 'Deployment', ...}],
|
||||
}
|
||||
```
|
||||
|
||||
**Strengths:** No Go required; fast iteration in any language.
|
||||
|
||||
**Weaknesses:** Lower ecosystem activity; not great for complex multi-CRD operators; webhook-based latency.
|
||||
|
||||
## KOPF (Python)
|
||||
|
||||
**What it is:** A Python framework for building operators. Async-first, decorator-based, no scaffolding step.
|
||||
|
||||
**Use when:**
|
||||
- Python shop
|
||||
- Operator logic is moderate complexity
|
||||
- Want fast iteration without recompilation
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
import kopf
|
||||
|
||||
@kopf.on.create('apps.example.com', 'v1alpha1', 'myapps')
|
||||
async def create_fn(spec, name, namespace, logger, **_):
|
||||
logger.info(f"creating MyApp {name}")
|
||||
# ... create children
|
||||
return {'phase': 'Ready'}
|
||||
|
||||
@kopf.on.delete('apps.example.com', 'v1alpha1', 'myapps')
|
||||
async def delete_fn(spec, name, namespace, **_):
|
||||
# cleanup external resources
|
||||
pass
|
||||
```
|
||||
|
||||
**Strengths:**
|
||||
- Async/await native (good for many concurrent reconciles)
|
||||
- No code generation
|
||||
- Good for ML/data teams already in Python
|
||||
|
||||
**Weaknesses:**
|
||||
- Smaller ecosystem than Go
|
||||
- Some features lag controller-runtime (e.g., complex caching)
|
||||
- Python startup cost in the controller pod
|
||||
|
||||
## java-operator-sdk
|
||||
|
||||
**What it is:** Java framework, Quarkus integration, modeled after controller-runtime.
|
||||
|
||||
**Use when:** JVM shop with strong Spring/Quarkus skills.
|
||||
|
||||
**Skip when:** You don't already have a JVM ops setup.
|
||||
|
||||
## Comparison: complexity vs control
|
||||
|
||||
```
|
||||
control ↑
|
||||
│ controller-runtime (full control, library)
|
||||
│ │
|
||||
│ kubebuilder (scaffolded controller-runtime)
|
||||
│ │
|
||||
│ operator-sdk Go (kubebuilder + OLM)
|
||||
│ │
|
||||
│ KOPF (Python decorators)
|
||||
│ │
|
||||
│ java-operator-sdk (JVM)
|
||||
│ │
|
||||
│ operator-sdk Ansible (playbooks)
|
||||
│ │
|
||||
│ operator-sdk Helm (chart-based)
|
||||
│ │
|
||||
│ metacontroller (webhook hooks)
|
||||
↓
|
||||
complexity ↓
|
||||
```
|
||||
|
||||
Higher control = more code, more flexibility. Lower complexity = faster start, less power.
|
||||
|
||||
## Cross-cutting concerns
|
||||
|
||||
Regardless of framework:
|
||||
|
||||
- **Webhooks for validation** — reject bad CRs at admission
|
||||
- **cert-manager** — rotate webhook certs automatically
|
||||
- **Prometheus** — `/metrics` endpoint via controller-runtime's built-in metrics
|
||||
- **OLM** (Operator Lifecycle Manager) — for OperatorHub publishing
|
||||
- **OperatorHub Capability Levels** — see `operator_capability_audit.py`
|
||||
|
||||
## Migration paths
|
||||
|
||||
| From | To | Effort |
|
||||
|---|---|---|
|
||||
| controller-runtime | kubebuilder | Low (kubebuilder uses controller-runtime) |
|
||||
| Helm chart | Helm-based operator-sdk | Low |
|
||||
| Helm chart | Go operator (kubebuilder) | High (rewrite logic in Go) |
|
||||
| KOPF | Go operator | High (language change) |
|
||||
| Any | metacontroller | Medium (move logic behind HTTP) |
|
||||
|
||||
## Selection checklist
|
||||
|
||||
Before committing:
|
||||
- [ ] Identify primary language constraint
|
||||
- [ ] Target environment (vanilla k8s vs OpenShift/OLM)
|
||||
- [ ] Operator complexity: 1 CRD vs many
|
||||
- [ ] Need webhooks?
|
||||
- [ ] Need OLM publishing?
|
||||
- [ ] Build a 1-week proof-of-concept; verify reconcile latency, status update flow, and dev-loop ergonomics
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Validate a Kubernetes CRD YAML against operator-pattern best practices.
|
||||
|
||||
Checks for status subresource, structural schema, conditions support, printer
|
||||
columns, version policy, and other operator-grade design rules. Stdlib-only —
|
||||
parses YAML via a minimal embedded reader (no PyYAML dependency).
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
CHECKS = [
|
||||
("status_subresource", "Each version must declare subresources.status (otherwise status updates loop spec reconciles)"),
|
||||
("storage_version", "Exactly one version must be storage:true"),
|
||||
("served_version", "At least one version must be served:true"),
|
||||
("schema_present", "Each version must declare schema.openAPIV3Schema"),
|
||||
("schema_typed", "Schema must declare 'type: object' at root (no x-kubernetes-preserve-unknown-fields at root)"),
|
||||
("conditions_array", "Schema should declare a conditions array under status (for metav1.Conditions)"),
|
||||
("printer_columns", "additionalPrinterColumns should include Age and a status indicator"),
|
||||
("scope", "scope should be Namespaced unless cluster-scoped is justified"),
|
||||
("singular_listkind", "names.singular and names.listKind must be declared"),
|
||||
]
|
||||
|
||||
|
||||
def _load_yaml_minimal(path):
|
||||
"""Yield top-level YAML documents from a multi-doc file as text blocks.
|
||||
|
||||
Stdlib-only — splits on '---' separators. We grep relevant fields with
|
||||
regex rather than fully parse. Crude but enough for the structural
|
||||
checks below; a full YAML parser would be the upgrade path."""
|
||||
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
||||
text = f.read()
|
||||
docs = re.split(r"^---\s*$", text, flags=re.MULTILINE)
|
||||
return [d for d in docs if d.strip()]
|
||||
|
||||
|
||||
def _is_crd_doc(doc):
|
||||
return bool(re.search(r"^kind:\s*CustomResourceDefinition\s*$", doc, re.MULTILINE))
|
||||
|
||||
|
||||
def _check_one(doc, path):
|
||||
findings = []
|
||||
has_status_sub = bool(re.search(r"subresources:\s*\n\s*status:\s*\{?\s*\}?", doc))
|
||||
if not has_status_sub:
|
||||
findings.append(("FAIL", "status_subresource", "no subresources.status block found"))
|
||||
storage_count = len(re.findall(r"storage:\s*true\b", doc))
|
||||
if storage_count != 1:
|
||||
findings.append(("FAIL", "storage_version", f"expected exactly 1 storage:true, found {storage_count}"))
|
||||
served_count = len(re.findall(r"served:\s*true\b", doc))
|
||||
if served_count < 1:
|
||||
findings.append(("FAIL", "served_version", "no served:true version"))
|
||||
if "openAPIV3Schema" not in doc:
|
||||
findings.append(("FAIL", "schema_present", "no openAPIV3Schema declared"))
|
||||
if re.search(r"x-kubernetes-preserve-unknown-fields:\s*true", doc):
|
||||
findings.append(("WARN", "schema_typed", "x-kubernetes-preserve-unknown-fields: true present (defeats validation)"))
|
||||
if "conditions" not in doc.lower():
|
||||
findings.append(("WARN", "conditions_array", "no conditions array referenced (Karpathy: declare an explicit shape)"))
|
||||
if "additionalPrinterColumns" not in doc:
|
||||
findings.append(("WARN", "printer_columns", "no additionalPrinterColumns (kubectl get UX is poor)"))
|
||||
elif not re.search(r"name:\s*Age\b", doc):
|
||||
findings.append(("WARN", "printer_columns", "additionalPrinterColumns missing Age column"))
|
||||
if not re.search(r"^\s*scope:\s*\w+", doc, re.MULTILINE):
|
||||
findings.append(("WARN", "scope", "scope not explicitly set"))
|
||||
if not re.search(r"^\s*singular:\s*[\w<]", doc, re.MULTILINE):
|
||||
findings.append(("WARN", "singular_listkind", "names.singular not declared"))
|
||||
if not re.search(r"^\s*listKind:\s*[\w<]", doc, re.MULTILINE):
|
||||
findings.append(("WARN", "singular_listkind", "names.listKind not declared"))
|
||||
return findings
|
||||
|
||||
|
||||
def _walk_yaml_files(root):
|
||||
if os.path.isfile(root):
|
||||
yield root
|
||||
return
|
||||
for r, _, files in os.walk(root):
|
||||
for f in files:
|
||||
if f.endswith((".yaml", ".yml")):
|
||||
yield os.path.join(r, f)
|
||||
|
||||
|
||||
def audit(target):
|
||||
results = []
|
||||
for path in _walk_yaml_files(target):
|
||||
for doc in _load_yaml_minimal(path):
|
||||
if not _is_crd_doc(doc):
|
||||
continue
|
||||
kind_match = re.search(r"kind:\s*(\w+)\s*$", doc, re.MULTILINE)
|
||||
crd_kind = kind_match.group(1) if kind_match else "?"
|
||||
name_match = re.search(r"^\s+name:\s*([\w.\-]+)\s*$", doc, re.MULTILINE)
|
||||
crd_name = name_match.group(1) if name_match else os.path.basename(path)
|
||||
findings = _check_one(doc, path)
|
||||
results.append({"path": path, "name": crd_name, "kind": crd_kind, "findings": findings})
|
||||
return results
|
||||
|
||||
|
||||
def render_text(results):
|
||||
if not results:
|
||||
print("No CRD documents found.")
|
||||
return 0
|
||||
fails = sum(1 for r in results for f in r["findings"] if f[0] == "FAIL")
|
||||
warns = sum(1 for r in results for f in r["findings"] if f[0] == "WARN")
|
||||
print(f"CRD Validator — {len(results)} CRD(s) inspected, {fails} FAIL, {warns} WARN")
|
||||
print("")
|
||||
for r in results:
|
||||
print(f"== {r['name']} ({r['path']})")
|
||||
if not r["findings"]:
|
||||
print(" PASS: all checks green")
|
||||
continue
|
||||
for level, key, msg in r["findings"]:
|
||||
print(f" [{level}] {key}: {msg}")
|
||||
print("")
|
||||
return 1 if fails else 0
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--crd", required=True, help="Path to a CRD YAML file or a directory of YAMLs")
|
||||
ap.add_argument("--format", choices=["text", "json"], default="text")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.exists(args.crd):
|
||||
print(f"ERROR: not found: {args.crd}", file=sys.stderr)
|
||||
return 2
|
||||
results = audit(args.crd)
|
||||
if args.format == "json":
|
||||
print(json.dumps(results, indent=2))
|
||||
return 0
|
||||
return render_text(results)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Score an operator against OperatorHub Capability Levels (1-5).
|
||||
|
||||
Walks an operator repo and detects evidence for each level. Level achieved =
|
||||
highest level for which all required signals are present. Reports next-level
|
||||
gaps as concrete advancement steps.
|
||||
|
||||
Levels:
|
||||
L1 Basic Install — CRD + controller + Deployment manifest
|
||||
L2 Seamless Upgrades — version conversion + PDB + leader election
|
||||
L3 Full Lifecycle — backup/restore + finalizers + status conditions
|
||||
L4 Deep Insights — /metrics endpoint + Prometheus rules
|
||||
L5 Auto Pilot — HPA / VPA / autotuning logic referenced
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
SIGNALS = {
|
||||
"L1": [
|
||||
("crd_present", lambda files, contents: any("CustomResourceDefinition" in c for c in contents.values())),
|
||||
("deployment_present", lambda files, contents: any(re.search(r"^kind:\s*Deployment", c, re.MULTILINE) for c in contents.values())),
|
||||
("controller_code", lambda files, contents: any(p.endswith(".go") and "Reconcile" in c for p, c in contents.items())),
|
||||
],
|
||||
"L2": [
|
||||
("conversion_webhook", lambda files, contents: any("conversion" in c.lower() and "webhook" in c.lower() for c in contents.values())),
|
||||
("leader_election", lambda files, contents: any("LeaderElection" in c or "leader-elect" in c for c in contents.values())),
|
||||
("pdb_present", lambda files, contents: any(re.search(r"kind:\s*PodDisruptionBudget", c) for c in contents.values())),
|
||||
],
|
||||
"L3": [
|
||||
("finalizers", lambda files, contents: any("Finalizer" in c or "finalizers" in c for c in contents.values())),
|
||||
("status_conditions", lambda files, contents: any("metav1.Condition" in c or "SetStatusCondition" in c for c in contents.values())),
|
||||
("backup_restore_hint", lambda files, contents: any(re.search(r"\b(backup|restore|snapshot)\b", c, re.IGNORECASE) for c in contents.values())),
|
||||
],
|
||||
"L4": [
|
||||
("metrics_endpoint", lambda files, contents: any(re.search(r"/metrics|prometheus", c) for c in contents.values())),
|
||||
("prometheus_rules", lambda files, contents: any(re.search(r"PrometheusRule|alert:", c) for c in contents.values())),
|
||||
],
|
||||
"L5": [
|
||||
("autoscaling_referenced", lambda files, contents: any(re.search(r"\bHorizontalPodAutoscaler|VerticalPodAutoscaler|autoscal", c) for c in contents.values())),
|
||||
("autotune_logic", lambda files, contents: any(re.search(r"autotune|self-heal|anomaly", c, re.IGNORECASE) for c in contents.values())),
|
||||
],
|
||||
}
|
||||
|
||||
LEVEL_NAMES = {
|
||||
"L1": "Basic Install",
|
||||
"L2": "Seamless Upgrades",
|
||||
"L3": "Full Lifecycle",
|
||||
"L4": "Deep Insights",
|
||||
"L5": "Auto Pilot",
|
||||
}
|
||||
|
||||
SCAN_EXTS = {".go", ".yaml", ".yml", ".md"}
|
||||
SKIP_DIRS = {".git", "node_modules", "vendor", "bin", "dist", "__pycache__"}
|
||||
|
||||
|
||||
def _walk(root):
|
||||
files = {}
|
||||
for r, dirs, fnames in os.walk(root):
|
||||
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
|
||||
for f in fnames:
|
||||
if os.path.splitext(f)[1] in SCAN_EXTS:
|
||||
p = os.path.join(r, f)
|
||||
try:
|
||||
with open(p, "r", encoding="utf-8", errors="replace") as fh:
|
||||
files[p] = fh.read()
|
||||
except OSError:
|
||||
continue
|
||||
return files
|
||||
|
||||
|
||||
def evaluate(operator_dir):
|
||||
contents = _walk(operator_dir)
|
||||
file_paths = list(contents.keys())
|
||||
results = {}
|
||||
achieved_max = None
|
||||
for level in ["L1", "L2", "L3", "L4", "L5"]:
|
||||
signals = SIGNALS[level]
|
||||
passing = []
|
||||
failing = []
|
||||
for key, check in signals:
|
||||
ok = check(file_paths, contents)
|
||||
(passing if ok else failing).append(key)
|
||||
all_pass = len(failing) == 0
|
||||
results[level] = {
|
||||
"name": LEVEL_NAMES[level],
|
||||
"passing": passing,
|
||||
"missing": failing,
|
||||
"achieved": all_pass,
|
||||
}
|
||||
if all_pass:
|
||||
achieved_max = level
|
||||
else:
|
||||
break
|
||||
return {"current_level": achieved_max, "details": results}
|
||||
|
||||
|
||||
def render_text(report, operator_dir):
|
||||
print(f"Operator Capability Audit — {operator_dir}")
|
||||
current = report["current_level"]
|
||||
if current is None:
|
||||
print("Current level: BELOW_L1 (no operator structure detected)")
|
||||
else:
|
||||
print(f"Current level: {current} — {LEVEL_NAMES[current]}")
|
||||
print("")
|
||||
for level in ["L1", "L2", "L3", "L4", "L5"]:
|
||||
d = report["details"].get(level)
|
||||
if d is None:
|
||||
continue
|
||||
marker = "✓" if d["achieved"] else "✗"
|
||||
print(f" {marker} {level} {d['name']}: pass={len(d['passing'])} miss={len(d['missing'])}")
|
||||
for k in d["missing"]:
|
||||
print(f" - missing: {k}")
|
||||
print("")
|
||||
next_level = None
|
||||
for lv in ["L1", "L2", "L3", "L4", "L5"]:
|
||||
if lv == current:
|
||||
continue
|
||||
if not report["details"].get(lv, {}).get("achieved"):
|
||||
next_level = lv
|
||||
break
|
||||
if next_level:
|
||||
misses = report["details"][next_level]["missing"]
|
||||
print(f"Next: advance to {next_level} ({LEVEL_NAMES[next_level]}) by addressing:")
|
||||
for k in misses:
|
||||
print(f" - {k}")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--operator-dir", required=True, help="Path to operator repo root")
|
||||
ap.add_argument("--format", choices=["text", "json"], default="text")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.isdir(args.operator_dir):
|
||||
print(f"ERROR: not a directory: {args.operator_dir}", file=sys.stderr)
|
||||
return 2
|
||||
report = evaluate(args.operator_dir)
|
||||
if args.format == "json":
|
||||
print(json.dumps(report, indent=2))
|
||||
else:
|
||||
render_text(report, args.operator_dir)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Lint a Go controller reconcile function for operator anti-patterns.
|
||||
|
||||
Detects common operator bugs from static patterns in Go source: blocking calls
|
||||
inside reconcile, spec mutation (instead of status), missing requeue on error,
|
||||
oversized reconcile functions, and missing finalizer/condition handling. Pure
|
||||
regex heuristics; not a Go AST parser, but catches the recurring mistakes.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
CODE_EXTS = {".go"}
|
||||
|
||||
|
||||
CHECKS = [
|
||||
("time_sleep", r"\btime\.Sleep\s*\(", "FAIL", "time.Sleep inside reconcile blocks the work queue. Use ctrl.Result{RequeueAfter: ...}."),
|
||||
("update_spec", r"r\.(?:Client\.)?Update\(\s*ctx\s*,\s*\w+\)", "WARN", "r.Client.Update on the reconciled object likely mutates spec. Use r.Status().Update for status."),
|
||||
("missing_context_in_http", r"http\.(?:Get|Post|Do)\s*\(", "WARN", "HTTP calls without ctx-aware client; cannot cancel during shutdown."),
|
||||
("os_exit", r"\bos\.Exit\s*\(", "FAIL", "os.Exit inside reconcile kills the controller; return an error instead."),
|
||||
("panic_call", r"\bpanic\s*\(", "WARN", "panic inside reconcile crashes the controller; return an error so it requeues."),
|
||||
("log_fatal", r"\blog\.Fatal", "FAIL", "log.Fatal exits the process; return an error instead."),
|
||||
]
|
||||
|
||||
|
||||
def _read(path):
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
||||
return f.read()
|
||||
except OSError:
|
||||
return ""
|
||||
|
||||
|
||||
def _find_reconcile_blocks(src):
|
||||
"""Return list of (start_line, end_line, body) for each Reconcile func."""
|
||||
blocks = []
|
||||
sig = re.compile(r"func\s+\([^)]*\)\s+Reconcile\s*\(", re.MULTILINE)
|
||||
for m in sig.finditer(src):
|
||||
start = m.start()
|
||||
i = src.find("{", m.end())
|
||||
if i < 0:
|
||||
continue
|
||||
depth = 1
|
||||
j = i + 1
|
||||
while j < len(src) and depth > 0:
|
||||
c = src[j]
|
||||
if c == "{":
|
||||
depth += 1
|
||||
elif c == "}":
|
||||
depth -= 1
|
||||
j += 1
|
||||
if depth == 0:
|
||||
body = src[i:j]
|
||||
start_line = src[:start].count("\n") + 1
|
||||
end_line = src[:j].count("\n") + 1
|
||||
blocks.append((start_line, end_line, body))
|
||||
return blocks
|
||||
|
||||
|
||||
def _check_block(body, start_line):
|
||||
findings = []
|
||||
for key, pattern, level, msg in CHECKS:
|
||||
for m in re.finditer(pattern, body):
|
||||
line_offset = body[: m.start()].count("\n")
|
||||
findings.append({
|
||||
"level": level,
|
||||
"key": key,
|
||||
"line": start_line + line_offset,
|
||||
"msg": msg,
|
||||
})
|
||||
body_lines = body.count("\n")
|
||||
if body_lines > 80:
|
||||
findings.append({
|
||||
"level": "WARN",
|
||||
"key": "reconcile_length",
|
||||
"line": start_line,
|
||||
"msg": f"Reconcile body is {body_lines} lines (>80). Extract reconcileXxx subroutines.",
|
||||
})
|
||||
has_finalizer_add = re.search(r"controllerutil\.AddFinalizer\b|finalizers\s*=", body)
|
||||
has_finalizer_remove = re.search(r"controllerutil\.RemoveFinalizer\b", body)
|
||||
if has_finalizer_add and not has_finalizer_remove:
|
||||
findings.append({
|
||||
"level": "WARN",
|
||||
"key": "finalizer_unbalanced",
|
||||
"line": start_line,
|
||||
"msg": "AddFinalizer found but no RemoveFinalizer call — orphaned external resources on delete.",
|
||||
})
|
||||
if not re.search(r"ctrl\.Result\{", body):
|
||||
findings.append({
|
||||
"level": "WARN",
|
||||
"key": "missing_requeue",
|
||||
"line": start_line,
|
||||
"msg": "Reconcile body does not return ctrl.Result{...}. Confirm error returns trigger requeue.",
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def audit_file(path):
|
||||
src = _read(path)
|
||||
if not src or "Reconcile" not in src:
|
||||
return []
|
||||
blocks = _find_reconcile_blocks(src)
|
||||
out = []
|
||||
for start_line, _, body in blocks:
|
||||
out.extend(_check_block(body, start_line))
|
||||
# Cross-function check: AddFinalizer present in file → RemoveFinalizer must be too.
|
||||
has_add = "controllerutil.AddFinalizer" in src or re.search(r"finalizers\s*=", src)
|
||||
has_remove = "controllerutil.RemoveFinalizer" in src
|
||||
if has_add and not has_remove:
|
||||
out = [f for f in out if f["key"] != "finalizer_unbalanced"]
|
||||
out.append({
|
||||
"level": "WARN",
|
||||
"key": "finalizer_unbalanced",
|
||||
"line": 0,
|
||||
"msg": "AddFinalizer is called somewhere in this file but RemoveFinalizer is not — orphaned external resources on delete.",
|
||||
})
|
||||
elif has_remove:
|
||||
# Suppress per-block warnings if file-level pairing is balanced.
|
||||
out = [f for f in out if f["key"] != "finalizer_unbalanced"]
|
||||
return out
|
||||
|
||||
|
||||
def _walk(target):
|
||||
if os.path.isfile(target):
|
||||
yield target
|
||||
return
|
||||
for r, _, files in os.walk(target):
|
||||
for f in files:
|
||||
if os.path.splitext(f)[1] in CODE_EXTS:
|
||||
yield os.path.join(r, f)
|
||||
|
||||
|
||||
def audit(target):
|
||||
results = []
|
||||
for path in _walk(target):
|
||||
findings = audit_file(path)
|
||||
if findings:
|
||||
results.append({"path": path, "findings": findings})
|
||||
return results
|
||||
|
||||
|
||||
def render_text(results):
|
||||
fails = sum(1 for r in results for f in r["findings"] if f["level"] == "FAIL")
|
||||
warns = sum(1 for r in results for f in r["findings"] if f["level"] == "WARN")
|
||||
print(f"Reconcile Lint — {len(results)} controller file(s), {fails} FAIL, {warns} WARN")
|
||||
print("")
|
||||
if not results:
|
||||
print("PASS: no anti-patterns detected.")
|
||||
return 0
|
||||
for r in results:
|
||||
print(f"== {r['path']}")
|
||||
for f in r["findings"]:
|
||||
print(f" [{f['level']}] line {f['line']} {f['key']}: {f['msg']}")
|
||||
print("")
|
||||
return 1 if fails else 0
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--controller", required=True, help="Path to a Go controller file or directory")
|
||||
ap.add_argument("--format", choices=["text", "json"], default="text")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.exists(args.controller):
|
||||
print(f"ERROR: not found: {args.controller}", file=sys.stderr)
|
||||
return 2
|
||||
results = audit(args.controller)
|
||||
if args.format == "json":
|
||||
print(json.dumps(results, indent=2))
|
||||
return 0
|
||||
return render_text(results)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
242
engineering/skills/kubernetes-operator/SKILL.md
Normal file
242
engineering/skills/kubernetes-operator/SKILL.md
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
---
|
||||
name: kubernetes-operator
|
||||
description: Use when building a Kubernetes Operator — custom controllers that reconcile CRD state. Triggers on "build an operator", "CRD design", "reconcile loop", "controller-runtime", "kubebuilder", "operator-sdk", "metacontroller", "KOPF", "operator capability levels", or "custom resource". Ships CRD validator, reconcile-loop linter, and OperatorHub capability auditor (all stdlib Python), 4 references on the operator pattern + CRD design + reconcile patterns + tooling landscape, and a /operator-audit slash command. NOT a generic k8s skill — specifically the Operator pattern.
|
||||
context: fork
|
||||
version: 2.4.0
|
||||
author: claude-code-skills
|
||||
license: MIT
|
||||
tags: [kubernetes, operator, crd, controller-runtime, kubebuilder, operator-sdk, metacontroller, kopf, reconcile, devops]
|
||||
compatible_tools: [claude-code, codex-cli, cursor, antigravity, opencode, gemini-cli]
|
||||
---
|
||||
|
||||
# Kubernetes Operator
|
||||
|
||||
Build operators that reconcile correctly. Most operator bugs are not Kubernetes bugs — they are reconcile-loop bugs: missing finalizers, blocking calls, no requeue on transient errors, status drift, RBAC over-grants. This skill catches them deterministically before they reach a cluster.
|
||||
|
||||
## When to use
|
||||
|
||||
- Building a new Kubernetes Operator (controller for a CRD)
|
||||
- Reviewing an existing operator for capability-level gaps
|
||||
- Auditing a CRD spec for status/conditions/finalizer correctness
|
||||
- Choosing a framework (controller-runtime / kubebuilder / operator-sdk / metacontroller / KOPF)
|
||||
- Designing the API surface of a Custom Resource
|
||||
- Hardening RBAC, leader election, or webhook validation
|
||||
|
||||
## When NOT to use
|
||||
|
||||
- Plain Helm chart packaging → use `helm-chart-builder`
|
||||
- Standard kubectl operations / blue-green deploys → use `senior-devops`
|
||||
- General k8s security posture → use `cloud-security`
|
||||
- "I want to run a workload" — that's a Deployment / Job, not an operator
|
||||
|
||||
## Core principle: an operator is a reconcile loop, not a script
|
||||
|
||||
```
|
||||
observe(actual) → desired = read(spec) → diff(actual, desired) → act → update(status)
|
||||
↓
|
||||
requeue / done
|
||||
```
|
||||
|
||||
Operators that fail are the ones that:
|
||||
1. Treat reconcile as imperative (do this, then this, then this) instead of declarative (make actual=desired, idempotently)
|
||||
2. Don't requeue transient failures
|
||||
3. Don't use finalizers, leaving orphan resources
|
||||
4. Mutate spec instead of status
|
||||
5. Don't use the status subresource (status updates trigger spec reconciles → loop)
|
||||
6. Block in reconcile (long HTTP calls, locks)
|
||||
7. Forget leader election → split-brain on multi-replica deploys
|
||||
|
||||
The 3 tools below catch each of these.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
SKILL=engineering/kubernetes-operator/skills/kubernetes-operator
|
||||
|
||||
# Validate a CRD design
|
||||
python "$SKILL/scripts/crd_validator.py" --crd config/crd/myapp.yaml
|
||||
|
||||
# Lint a Go reconcile function
|
||||
python "$SKILL/scripts/reconcile_lint.py" --controller controllers/myapp_controller.go
|
||||
|
||||
# Score against OperatorHub Capability Levels (1-5)
|
||||
python "$SKILL/scripts/operator_capability_audit.py" --operator-dir .
|
||||
```
|
||||
|
||||
## The 3 Python tools
|
||||
|
||||
All stdlib-only. Run with `--help`.
|
||||
|
||||
### `crd_validator.py`
|
||||
|
||||
Validates a CRD YAML against operator-pattern best practices.
|
||||
|
||||
```bash
|
||||
python scripts/crd_validator.py --crd config/crd/myapp.yaml
|
||||
python scripts/crd_validator.py --crd config/crd/ --format json
|
||||
```
|
||||
|
||||
**Checks:**
|
||||
- `spec.versions[*].subresources.status` is set (status subresource)
|
||||
- `spec.scope` is `Namespaced` (not `Cluster`) unless explicitly justified
|
||||
- Singular and listKind defined
|
||||
- `spec.versions[*].schema.openAPIV3Schema` has type definitions (no `x-kubernetes-preserve-unknown-fields: true` at top level)
|
||||
- A version is marked `served: true` AND `storage: true`
|
||||
- Conditions array is in the schema (allows `metav1.Conditions`)
|
||||
- Printer columns include `Age` and `Status`/`Phase`
|
||||
|
||||
### `reconcile_lint.py`
|
||||
|
||||
Lints a Go controller reconcile function for anti-patterns.
|
||||
|
||||
```bash
|
||||
python scripts/reconcile_lint.py --controller controllers/myapp_controller.go
|
||||
```
|
||||
|
||||
**Checks (regex-based heuristics):**
|
||||
- Returns are `(ctrl.Result, error)` shape
|
||||
- Errors trigger a non-zero requeue (`return ctrl.Result{Requeue: true}, err`)
|
||||
- `client.Update()` on the spec object is flagged (controllers should update only status)
|
||||
- `time.Sleep` inside reconcile is flagged (use `RequeueAfter`)
|
||||
- HTTP calls without context cancellation are flagged
|
||||
- Missing `defer` after a finalizer add
|
||||
- No `IsConditionTrue` / `SetCondition` calls when conditions present in CRD
|
||||
- Reconcile function exceeds 80 lines (extract subroutines)
|
||||
|
||||
### `operator_capability_audit.py`
|
||||
|
||||
Scores an operator against OperatorHub's 5 Capability Levels.
|
||||
|
||||
```bash
|
||||
python scripts/operator_capability_audit.py --operator-dir .
|
||||
```
|
||||
|
||||
**Levels:**
|
||||
- **L1 — Basic Install:** CRD defined, controller deploys it
|
||||
- **L2 — Seamless Upgrades:** PDBs, conversion webhooks, version skew strategy
|
||||
- **L3 — Full Lifecycle:** backups, restores, failure recovery
|
||||
- **L4 — Deep Insights:** metrics endpoint, Prometheus rules, alerts
|
||||
- **L5 — Auto Pilot:** auto-scaling, auto-tuning, anomaly detection
|
||||
|
||||
Reports current level + concrete next steps to advance one level.
|
||||
|
||||
## Tooling landscape
|
||||
|
||||
Pick a framework based on language and complexity. See `references/tooling_landscape.md`.
|
||||
|
||||
| Framework | Language | Best for | Maintenance |
|
||||
|---|---|---|---|
|
||||
| **controller-runtime** | Go | Production-grade, low-level control | Active (sig-api-machinery) |
|
||||
| **kubebuilder** | Go | Standard scaffolding, opinionated | Active (Kubernetes SIGs) |
|
||||
| **operator-sdk** | Go / Helm / Ansible | OpenShift / mixed-paradigm teams | Active (Red Hat) |
|
||||
| **metacontroller** | Any (webhook-based) | Polyglot teams, avoiding Go | Less active |
|
||||
| **KOPF** | Python | Python shops, async-first | Active (community) |
|
||||
| **java-operator-sdk** | Java | JVM shops | Active (Red Hat / Java SIG) |
|
||||
|
||||
Decision rules:
|
||||
- New operator + Go shop → kubebuilder
|
||||
- New operator + Python shop → KOPF
|
||||
- New operator + can't pick a language → metacontroller
|
||||
- OpenShift target → operator-sdk
|
||||
|
||||
## CRD design principles
|
||||
|
||||
See `references/crd_design.md` for full detail. Quick rules:
|
||||
|
||||
1. **status is the source of truth for the controller's view of the world.** Spec is what the user wants; status is what the controller observed.
|
||||
2. **Use the status subresource.** Without it, status updates re-trigger reconcile (loop).
|
||||
3. **Use Conditions.** `Ready`, `Reconciling`, `Degraded`. Each carries a reason and message.
|
||||
4. **Add finalizers.** Without finalizers, deletion races the controller and orphans external resources.
|
||||
5. **Version your CRD from day 1.** `v1alpha1` → `v1beta1` → `v1`. Plan a conversion webhook.
|
||||
6. **Validate via OpenAPI v3 schema.** Don't rely on the controller for validation that should fail at admission.
|
||||
7. **Use `additionalPrinterColumns` for `kubectl get`.** Show `Age`, `Phase`, `Ready` at minimum.
|
||||
8. **Namespace your CRDs unless they manage cluster-scoped resources.**
|
||||
|
||||
## Reconcile loop principles
|
||||
|
||||
See `references/reconcile_loop.md` for full detail. Quick rules:
|
||||
|
||||
1. **Idempotent.** Reconciling the same state twice → same result, zero side effects.
|
||||
2. **Read once, decide, act.** Don't observe the world repeatedly during reconcile.
|
||||
3. **Update status, not spec.** Spec belongs to the user.
|
||||
4. **Return errors that requeue.** Use `ctrl.Result{RequeueAfter: ...}` for known transient cases.
|
||||
5. **Never block.** No `time.Sleep`. No long HTTP calls without context.
|
||||
6. **Use the cache.** Read via the controller's cached client; only escape the cache for a specific reason.
|
||||
7. **Leader-elect when running >1 replica.** Otherwise enable single-replica mode.
|
||||
8. **Set OwnerReferences.** Cascading deletion is the operator pattern's free gift.
|
||||
|
||||
## Workflows
|
||||
|
||||
### Workflow 1: Bootstrap a new operator (Go + kubebuilder)
|
||||
|
||||
```
|
||||
1. Pick a Group/Version/Kind: e.g., apps.example.com/v1alpha1, kind=MyApp
|
||||
2. kubebuilder init --domain example.com --repo github.com/org/myapp-operator
|
||||
3. kubebuilder create api --group apps --version v1alpha1 --kind MyApp
|
||||
4. Run crd_validator.py on config/crd/bases/apps.example.com_myapps.yaml
|
||||
→ Fix every WARN before writing controller code
|
||||
5. Implement the reconcile function (Karpathy principle 2: simplest correct version first)
|
||||
6. Run reconcile_lint.py on controllers/myapp_controller.go
|
||||
7. Run operator_capability_audit.py --operator-dir . — confirm L1
|
||||
8. Test in a kind cluster: kubectl apply -f config/samples/
|
||||
9. Add status conditions; aim for L2 in the same PR
|
||||
```
|
||||
|
||||
### Workflow 2: Audit an existing operator
|
||||
|
||||
```
|
||||
1. Run operator_capability_audit.py --operator-dir <path>
|
||||
2. Run crd_validator.py --crd config/crd/
|
||||
3. Run reconcile_lint.py --controller controllers/
|
||||
4. Triage findings:
|
||||
- FAIL → block release; fix before next deploy
|
||||
- WARN → file an issue; fix in next 30 days
|
||||
5. Document current capability level in README; commit
|
||||
6. Plan one capability level advancement per quarter
|
||||
```
|
||||
|
||||
### Workflow 3: Choose a framework
|
||||
|
||||
```
|
||||
1. Identify primary language constraint (team skill)
|
||||
2. Identify deployment target (vanilla k8s vs OpenShift)
|
||||
3. Identify operator complexity (single CRD vs multi-CRD vs cluster-wide)
|
||||
4. Cross-reference with references/tooling_landscape.md
|
||||
5. Build a 1-week proof-of-concept before committing
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- `references/operator_pattern.md` — what an operator IS, when to use vs alternatives
|
||||
- `references/crd_design.md` — CRD design principles, versioning, conversion webhooks
|
||||
- `references/reconcile_loop.md` — reconcile patterns, error handling, idempotency
|
||||
- `references/tooling_landscape.md` — framework comparison + decision tree
|
||||
|
||||
## Slash command
|
||||
|
||||
`/operator-audit` — Run all 3 tools on an operator repo and produce a markdown report.
|
||||
|
||||
## Asset templates
|
||||
|
||||
- `assets/crd_template.yaml` — CRD with status subresource, conditions, finalizer hint, printer columns
|
||||
- `assets/reconcile_skeleton.go` — Go controller reconcile function with idempotency, conditions, finalizers, requeue patterns
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- **`time.Sleep(30 * time.Second)` inside reconcile** — block other reconciles. Use `RequeueAfter`.
|
||||
- **`r.Client.Update(ctx, obj)` to set status** — use `r.Status().Update(ctx, obj)` instead.
|
||||
- **No leader election + 2+ replicas** — split-brain.
|
||||
- **No finalizer** — external resources orphan on deletion.
|
||||
- **CRD without status subresource** — status updates trigger spec reconciles (infinite loop).
|
||||
- **Reconcile function > 200 lines** — extract reconcileXxx subroutines per condition.
|
||||
- **`x-kubernetes-preserve-unknown-fields: true` on spec root** — defeats validation.
|
||||
- **Imperative reconcile** — "if creating, do A; if updating, do B; if deleting, do C". Wrong shape. Reconcile = make actual=desired, regardless of how we got here.
|
||||
|
||||
## Verifiable success
|
||||
|
||||
A team using this skill should achieve:
|
||||
|
||||
- 100% of new CRDs pass `crd_validator.py` before merge
|
||||
- All reconcile functions pass `reconcile_lint.py` strict mode
|
||||
- Operators reach OperatorHub Capability Level 3 (Full Lifecycle) before public release
|
||||
- Mean time to fix a reconcile bug: <1 day (no infinite loops in production)
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
# Production CRD template — passes crd_validator.py
|
||||
# Fill in <PLACEHOLDERS>; remove these comments before applying.
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: <plural>.<group> # e.g., myapps.apps.example.com
|
||||
spec:
|
||||
group: <group> # e.g., apps.example.com
|
||||
names:
|
||||
kind: <Kind> # e.g., MyApp
|
||||
plural: <plural> # e.g., myapps
|
||||
singular: <singular> # e.g., myapp
|
||||
listKind: <Kind>List # e.g., MyAppList
|
||||
shortNames: [<short>] # optional, 2-3 letters
|
||||
scope: Namespaced # default; Cluster requires justification
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
storage: true
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
required: [version]
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
pattern: '^[0-9]+\.[0-9]+\.[0-9]+$'
|
||||
description: Semver version of the application
|
||||
replicas:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 3
|
||||
description: Number of replicas to run
|
||||
status:
|
||||
type: object
|
||||
properties:
|
||||
phase:
|
||||
type: string
|
||||
enum: [Pending, Running, Failed]
|
||||
observedGeneration:
|
||||
type: integer
|
||||
description: Spec generation last reconciled
|
||||
conditions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [type, status, lastTransitionTime]
|
||||
properties:
|
||||
type: { type: string }
|
||||
status: { type: string, enum: ["True", "False", "Unknown"] }
|
||||
reason: { type: string }
|
||||
message: { type: string }
|
||||
lastTransitionTime: { type: string, format: date-time }
|
||||
observedGeneration: { type: integer }
|
||||
subresources:
|
||||
status: {} # CRITICAL — enables /status subresource
|
||||
additionalPrinterColumns:
|
||||
- name: Phase
|
||||
type: string
|
||||
jsonPath: .status.phase
|
||||
- name: Ready
|
||||
type: string
|
||||
jsonPath: .status.conditions[?(@.type=="Ready")].status
|
||||
- name: Age
|
||||
type: date
|
||||
jsonPath: .metadata.creationTimestamp
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
// Reconcile skeleton — passes reconcile_lint.py.
|
||||
// Replace <PLACEHOLDER> markers; rename receiver + types to match your CR.
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
|
||||
appsv1alpha1 "<MODULE>/api/v1alpha1"
|
||||
)
|
||||
|
||||
const finalizerName = "<group>/finalizer"
|
||||
|
||||
type MyAppReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx).WithValues("myapp", req.NamespacedName)
|
||||
|
||||
var cr appsv1alpha1.MyApp
|
||||
if err := r.Get(ctx, req.NamespacedName, &cr); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if !cr.DeletionTimestamp.IsZero() {
|
||||
return r.reconcileDelete(ctx, &cr)
|
||||
}
|
||||
|
||||
if !controllerutil.ContainsFinalizer(&cr, finalizerName) {
|
||||
controllerutil.AddFinalizer(&cr, finalizerName)
|
||||
return ctrl.Result{}, r.Update(ctx, &cr)
|
||||
}
|
||||
|
||||
meta.SetStatusCondition(&cr.Status.Conditions, metav1.Condition{
|
||||
Type: "Reconciling",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "InProgress",
|
||||
Message: "Converging to desired state",
|
||||
ObservedGeneration: cr.Generation,
|
||||
})
|
||||
|
||||
res, recErr := r.reconcileNormal(ctx, &cr)
|
||||
|
||||
if recErr == nil {
|
||||
meta.SetStatusCondition(&cr.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready", Status: metav1.ConditionTrue,
|
||||
Reason: "AllReady", Message: "all components healthy",
|
||||
ObservedGeneration: cr.Generation,
|
||||
})
|
||||
} else {
|
||||
meta.SetStatusCondition(&cr.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready", Status: metav1.ConditionFalse,
|
||||
Reason: "ReconcileError", Message: recErr.Error(),
|
||||
ObservedGeneration: cr.Generation,
|
||||
})
|
||||
}
|
||||
|
||||
cr.Status.ObservedGeneration = cr.Generation
|
||||
|
||||
if statusErr := r.Status().Update(ctx, &cr); statusErr != nil {
|
||||
logger.Error(statusErr, "failed to update status")
|
||||
return res, errors.Join(recErr, statusErr)
|
||||
}
|
||||
return res, recErr
|
||||
}
|
||||
|
||||
func (r *MyAppReconciler) reconcileNormal(ctx context.Context, cr *appsv1alpha1.MyApp) (ctrl.Result, error) {
|
||||
// Idempotent: read desired, build child, CreateOrUpdate.
|
||||
deployment := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: cr.Name, Namespace: cr.Namespace}}
|
||||
op, err := controllerutil.CreateOrUpdate(ctx, r.Client, deployment, func() error {
|
||||
deployment.Spec.Replicas = &cr.Spec.Replicas
|
||||
// Build container spec from cr.Spec — extracted helper for clarity
|
||||
// deployment.Spec.Template.Spec.Containers = buildContainers(&cr.Spec)
|
||||
return controllerutil.SetControllerReference(cr, deployment, r.Scheme)
|
||||
})
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
log.FromContext(ctx).Info("deployment", "operation", op)
|
||||
|
||||
// Periodic resync — keeps status fresh even when nothing changes.
|
||||
return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
|
||||
}
|
||||
|
||||
func (r *MyAppReconciler) reconcileDelete(ctx context.Context, cr *appsv1alpha1.MyApp) (ctrl.Result, error) {
|
||||
if !controllerutil.ContainsFinalizer(cr, finalizerName) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
if err := r.deleteExternalResources(ctx, cr); err != nil {
|
||||
return ctrl.Result{RequeueAfter: 30 * time.Second}, err
|
||||
}
|
||||
controllerutil.RemoveFinalizer(cr, finalizerName)
|
||||
return ctrl.Result{}, r.Update(ctx, cr)
|
||||
}
|
||||
|
||||
func (r *MyAppReconciler) deleteExternalResources(ctx context.Context, cr *appsv1alpha1.MyApp) error {
|
||||
// Implement teardown of external state (cloud DB, S3 bucket, DNS record, ...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *MyAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&appsv1alpha1.MyApp{}).
|
||||
Owns(&appsv1.Deployment{}).
|
||||
WithEventFilter(predicate.GenerationChangedPredicate{}).
|
||||
Complete(r)
|
||||
}
|
||||
196
engineering/skills/kubernetes-operator/references/crd_design.md
Normal file
196
engineering/skills/kubernetes-operator/references/crd_design.md
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
# CRD design
|
||||
|
||||
Custom Resource Definitions (CRDs) define the API surface of your operator. A bad CRD design locks you into hard-to-evolve schemas, forces wrapper APIs, and creates user-facing UX problems via `kubectl`.
|
||||
|
||||
## Anatomy of a production CRD
|
||||
|
||||
```yaml
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: myapps.apps.example.com # plural.group
|
||||
spec:
|
||||
group: apps.example.com
|
||||
names:
|
||||
kind: MyApp # PascalCase
|
||||
plural: myapps # lowercase
|
||||
singular: myapp # lowercase
|
||||
listKind: MyAppList # KindList
|
||||
shortNames: [ma] # optional
|
||||
scope: Namespaced # or Cluster (justify)
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
storage: true
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
required: [version]
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
pattern: '^[0-9]+\.[0-9]+\.[0-9]+$'
|
||||
replicas:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 3
|
||||
status:
|
||||
type: object
|
||||
properties:
|
||||
phase:
|
||||
type: string
|
||||
enum: [Pending, Running, Failed]
|
||||
conditions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [type, status, lastTransitionTime]
|
||||
properties:
|
||||
type: { type: string }
|
||||
status: { type: string, enum: ["True", "False", "Unknown"] }
|
||||
reason: { type: string }
|
||||
message: { type: string }
|
||||
lastTransitionTime: { type: string, format: date-time }
|
||||
observedGeneration: { type: integer }
|
||||
subresources:
|
||||
status: {} # CRITICAL — see below
|
||||
scale: # if scaling is meaningful
|
||||
specReplicasPath: .spec.replicas
|
||||
statusReplicasPath: .status.readyReplicas
|
||||
additionalPrinterColumns:
|
||||
- name: Phase
|
||||
type: string
|
||||
jsonPath: .status.phase
|
||||
- name: Ready
|
||||
type: string
|
||||
jsonPath: .status.conditions[?(@.type=="Ready")].status
|
||||
- name: Age
|
||||
type: date
|
||||
jsonPath: .metadata.creationTimestamp
|
||||
```
|
||||
|
||||
## Required structural elements
|
||||
|
||||
### 1. Status subresource — `subresources.status: {}`
|
||||
|
||||
Without it:
|
||||
- `r.Status().Update(ctx, obj)` doesn't work — falls back to `r.Update`
|
||||
- Status updates re-trigger spec reconcile → loop
|
||||
- RBAC can't be split between spec writers and status writers
|
||||
|
||||
**Always declare it.**
|
||||
|
||||
### 2. Conditions array
|
||||
|
||||
Use the standard `metav1.Condition` shape. Required fields: `type`, `status`, `lastTransitionTime`. Recommended: `reason`, `message`, `observedGeneration`.
|
||||
|
||||
Conventional condition types:
|
||||
- `Ready` — overall readiness
|
||||
- `Reconciling` — controller is actively working
|
||||
- `Degraded` — operating but with reduced capability
|
||||
- `Progressing` — change in progress (mostly for Deployments-style flows)
|
||||
|
||||
Use `meta.SetStatusCondition()` from `k8s.io/apimachinery/pkg/api/meta` — don't write to the slice directly.
|
||||
|
||||
### 3. observedGeneration
|
||||
|
||||
Track which spec generation the controller has acted on:
|
||||
|
||||
```go
|
||||
status.ObservedGeneration = obj.Generation
|
||||
```
|
||||
|
||||
Lets users tell whether status reflects the latest spec or a previous one.
|
||||
|
||||
### 4. Printer columns
|
||||
|
||||
`kubectl get myapp` UX is determined by `additionalPrinterColumns`. Always include:
|
||||
- `Phase` or `Ready` (status)
|
||||
- `Age` (so users know when it was created)
|
||||
|
||||
Optionally: replicas, version, key spec field.
|
||||
|
||||
### 5. Validation in the schema, not the controller
|
||||
|
||||
Express constraints declaratively:
|
||||
|
||||
| Constraint | OpenAPI |
|
||||
|---|---|
|
||||
| Range | `minimum`/`maximum` |
|
||||
| String pattern | `pattern: '^...$'` |
|
||||
| Enum | `enum: [Pending, Running]` |
|
||||
| Required field | `required: [...]` |
|
||||
| Default value | `default: 3` |
|
||||
| Min/max length | `minLength`/`maxLength` |
|
||||
|
||||
Reserve controller validation for cross-field rules and external dependencies (e.g., "this name is taken in our DB").
|
||||
|
||||
### 6. Avoid `x-kubernetes-preserve-unknown-fields: true`
|
||||
|
||||
It disables structural validation. Sometimes needed (e.g., raw `kubectl apply` patches), but never at the spec root. Use it sparingly on a single sub-tree.
|
||||
|
||||
## Versioning strategy
|
||||
|
||||
CRDs evolve. Plan from day 1:
|
||||
|
||||
| Stage | Version | Stability | Allowed changes |
|
||||
|---|---|---|---|
|
||||
| Internal preview | `v1alpha1` | None | Anything; document breaking changes |
|
||||
| Beta | `v1beta1` | Some | Additive only; deprecate fields |
|
||||
| GA | `v1` | Strong | Additive only; never remove fields |
|
||||
|
||||
Conversion webhook required when:
|
||||
- Multiple versions are served simultaneously
|
||||
- A field's shape changed between versions
|
||||
|
||||
For simple field renames, `x-kubernetes-conversion-strategy: None` works.
|
||||
|
||||
## Scope: Namespaced vs Cluster
|
||||
|
||||
Default to **Namespaced**. Cluster-scoped CRDs:
|
||||
- Can't be RBAC-restricted by namespace
|
||||
- Can't have `OwnerReferences` from namespaced parents
|
||||
- Are appropriate only for cluster-wide resources (`StorageClass`-like things)
|
||||
|
||||
If your operator manages namespace-bound things (apps, databases, queues), use Namespaced.
|
||||
|
||||
## Naming
|
||||
|
||||
- **Group**: `<domain>.<reverse-domain>` — e.g., `apps.example.com`. Don't use generic groups (`com`, `io`).
|
||||
- **Kind**: PascalCase, singular, descriptive — `MyApp`, `Database`, `Cache`. Avoid `MyAppResource` (the `Resource` suffix is implicit).
|
||||
- **Plural**: lowercase, plural — `myapps`, `databases`, `caches`.
|
||||
- **Short name**: 2-3 letters; check for conflicts with built-in resources.
|
||||
|
||||
## Validation tooling
|
||||
|
||||
- `kubectl apply --dry-run=server` — validates against your CRD
|
||||
- `kubectl explain <kind>.<field>` — shows what your schema documents
|
||||
- `crd_validator.py` — this skill's tool, structural rules
|
||||
|
||||
## Documentation in the schema
|
||||
|
||||
Use the `description` field on every property. `kubectl explain` reads it:
|
||||
|
||||
```yaml
|
||||
properties:
|
||||
replicas:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: |
|
||||
Number of replicas to run. Production deployments should use ≥3.
|
||||
Increases above 100 require quota approval.
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- **Top-level `x-kubernetes-preserve-unknown-fields: true`** — defeats validation
|
||||
- **No `scope:` declared** — defaults to namespaced but make intent explicit
|
||||
- **No printer columns** — `kubectl get` shows only `NAME AGE`
|
||||
- **Conditions written by hand** (not via `SetStatusCondition`) — easy to lose `lastTransitionTime`
|
||||
- **Status fields that duplicate spec** — keep them separate
|
||||
- **Using `metadata.annotations` to encode operator state** — use status fields
|
||||
- **Single huge CRD with 50+ fields** — split into multiple CRDs (e.g., MyApp + MyAppBackup + MyAppRestore)
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
# 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:
|
||||
|
||||
```yaml
|
||||
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.
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
# The reconcile loop
|
||||
|
||||
Reconcile is the heart of an operator. Most operator bugs are reconcile-loop bugs. The patterns below are deterministic — copy them.
|
||||
|
||||
## Skeleton — `Reconcile(ctx, req)`
|
||||
|
||||
```go
|
||||
func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
log := log.FromContext(ctx)
|
||||
|
||||
// 1. Fetch the CR
|
||||
var cr appsv1alpha1.MyApp
|
||||
if err := r.Get(ctx, req.NamespacedName, &cr); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return ctrl.Result{}, nil // CR is gone; nothing to do
|
||||
}
|
||||
return ctrl.Result{}, err // transient error → requeue
|
||||
}
|
||||
|
||||
// 2. Handle deletion via finalizer
|
||||
if !cr.DeletionTimestamp.IsZero() {
|
||||
return r.reconcileDelete(ctx, &cr)
|
||||
}
|
||||
if !controllerutil.ContainsFinalizer(&cr, finalizerName) {
|
||||
controllerutil.AddFinalizer(&cr, finalizerName)
|
||||
return ctrl.Result{}, r.Update(ctx, &cr)
|
||||
}
|
||||
|
||||
// 3. Mark Reconciling
|
||||
meta.SetStatusCondition(&cr.Status.Conditions, metav1.Condition{
|
||||
Type: "Reconciling", Status: metav1.ConditionTrue,
|
||||
Reason: "InProgress", Message: "Converging to desired state",
|
||||
ObservedGeneration: cr.Generation,
|
||||
})
|
||||
|
||||
// 4. Do the work, idempotently
|
||||
res, err := r.reconcileNormal(ctx, &cr)
|
||||
|
||||
// 5. Update status (always — even on error)
|
||||
if statusErr := r.Status().Update(ctx, &cr); statusErr != nil {
|
||||
log.Error(statusErr, "failed to update status")
|
||||
return res, errors.Join(err, statusErr)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
```
|
||||
|
||||
## The 5-step shape
|
||||
|
||||
1. **Fetch the CR.** Handle `NotFound` cleanly — the CR may have been deleted between event and reconcile.
|
||||
2. **Handle deletion.** If `DeletionTimestamp` is set, run cleanup, remove finalizer, return.
|
||||
3. **Set Reconciling condition.** Mark that the controller is working.
|
||||
4. **Do work idempotently.** Use `CreateOrUpdate`, compare desired-vs-actual, only act on differences.
|
||||
5. **Update status.** Even on error — partial progress is signal.
|
||||
|
||||
## Idempotence patterns
|
||||
|
||||
### Pattern: CreateOrUpdate
|
||||
|
||||
```go
|
||||
deployment := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: cr.Name, Namespace: cr.Namespace}}
|
||||
op, err := controllerutil.CreateOrUpdate(ctx, r.Client, deployment, func() error {
|
||||
deployment.Spec.Replicas = &cr.Spec.Replicas
|
||||
deployment.Spec.Template.Spec.Containers = buildContainers(&cr.Spec)
|
||||
return controllerutil.SetControllerReference(&cr, deployment, r.Scheme)
|
||||
})
|
||||
if err != nil { return ctrl.Result{}, err }
|
||||
log.Info("deployment", "operation", op) // "created", "updated", or "unchanged"
|
||||
```
|
||||
|
||||
This pattern is idempotent by construction.
|
||||
|
||||
### Pattern: SetControllerReference
|
||||
|
||||
Always set the OwnerReference so cascading deletion works:
|
||||
|
||||
```go
|
||||
controllerutil.SetControllerReference(&cr, child, r.Scheme)
|
||||
```
|
||||
|
||||
### Pattern: Finalizer for external resources
|
||||
|
||||
```go
|
||||
const finalizerName = "myapp.apps.example.com/finalizer"
|
||||
|
||||
func (r *MyAppReconciler) reconcileDelete(ctx context.Context, cr *appsv1alpha1.MyApp) (ctrl.Result, error) {
|
||||
if !controllerutil.ContainsFinalizer(cr, finalizerName) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
if err := r.deleteExternalResources(ctx, cr); err != nil {
|
||||
return ctrl.Result{RequeueAfter: 30 * time.Second}, err
|
||||
}
|
||||
controllerutil.RemoveFinalizer(cr, finalizerName)
|
||||
return ctrl.Result{}, r.Update(ctx, cr)
|
||||
}
|
||||
```
|
||||
|
||||
## Error handling and requeue
|
||||
|
||||
| Situation | Return |
|
||||
|---|---|
|
||||
| Permanent error (bad spec) | `ctrl.Result{}, nil` + condition with reason |
|
||||
| Transient error (API timeout, throttling) | `ctrl.Result{}, err` (auto-requeue with backoff) |
|
||||
| Need a retry in N seconds | `ctrl.Result{RequeueAfter: 30*time.Second}, nil` |
|
||||
| Done; no follow-up | `ctrl.Result{}, nil` |
|
||||
|
||||
**Don't use `time.Sleep` inside reconcile.** It blocks the work queue, starving other reconciles. Use `RequeueAfter`.
|
||||
|
||||
## Status update patterns
|
||||
|
||||
```go
|
||||
// Set a condition
|
||||
meta.SetStatusCondition(&cr.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready", Status: metav1.ConditionTrue,
|
||||
Reason: "AllReady", Message: "all components healthy",
|
||||
ObservedGeneration: cr.Generation,
|
||||
})
|
||||
|
||||
// Track observed generation
|
||||
cr.Status.ObservedGeneration = cr.Generation
|
||||
|
||||
// Update status — uses /status subresource
|
||||
if err := r.Status().Update(ctx, &cr); err != nil { ... }
|
||||
```
|
||||
|
||||
**Never** call `r.Update(ctx, &cr)` to update status. It uses the spec subresource, which the user owns.
|
||||
|
||||
## Read once, decide, act
|
||||
|
||||
Don't observe the world repeatedly during reconcile. The cache is read-only and consistent within a single reconcile pass:
|
||||
|
||||
```go
|
||||
// Good: read once, decide, act
|
||||
var pods corev1.PodList
|
||||
r.List(ctx, &pods, client.InNamespace(cr.Namespace), client.MatchingLabels{"app": cr.Name})
|
||||
desired := computeDesired(&cr, &pods)
|
||||
applyDesired(ctx, r.Client, desired)
|
||||
|
||||
// Bad: observe-act-observe-act
|
||||
for _, container := range cr.Spec.Containers {
|
||||
pod := r.Get(...) // re-reading the cache
|
||||
if needsRestart(pod) {
|
||||
r.Delete(...)
|
||||
pod = r.Get(...) // again
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Predicates — filter events you don't care about
|
||||
|
||||
```go
|
||||
func (r *MyAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&appsv1alpha1.MyApp{}).
|
||||
Owns(&appsv1.Deployment{}).
|
||||
WithEventFilter(predicate.GenerationChangedPredicate{}). // ignore status-only updates
|
||||
Complete(r)
|
||||
}
|
||||
```
|
||||
|
||||
`GenerationChangedPredicate` skips reconciles when only status changed — important to avoid loops.
|
||||
|
||||
## Leader election
|
||||
|
||||
Always enable leader election when running >1 controller replica:
|
||||
|
||||
```go
|
||||
mgr, _ := manager.New(cfg, manager.Options{
|
||||
LeaderElection: true,
|
||||
LeaderElectionID: "myapp-operator-leader",
|
||||
})
|
||||
```
|
||||
|
||||
Without it: split-brain. Two controllers both think they own the resource and fight.
|
||||
|
||||
## Performance — bounded reconcile time
|
||||
|
||||
A reconcile pass should complete in <30s for typical work, <2min for heavy work. Longer = the work queue starves other reconciles.
|
||||
|
||||
If work takes longer:
|
||||
- Break into phases; emit `RequeueAfter` between them
|
||||
- Move long-running work to a separate process (Job)
|
||||
- Cache expensive computations on `cr.Status`
|
||||
|
||||
## Logging conventions
|
||||
|
||||
```go
|
||||
log := log.FromContext(ctx).WithValues("phase", "create-deployment")
|
||||
log.Info("creating deployment", "name", cr.Name)
|
||||
log.Error(err, "failed to create deployment")
|
||||
```
|
||||
|
||||
- Use `log.FromContext(ctx)` — picks up controller-runtime's contextual logger
|
||||
- Use `Info` for normal flow, `Error` for retryable failures
|
||||
- Add structured fields, not formatted strings
|
||||
|
||||
## Anti-patterns checklist
|
||||
|
||||
- `time.Sleep` inside reconcile → starves queue; use `RequeueAfter`
|
||||
- `os.Exit` / `log.Fatal` → kills the controller; return an error
|
||||
- `panic` → same; return an error
|
||||
- `r.Update` to set status → use `r.Status().Update`
|
||||
- `r.Update` of the CR while the user could be editing it → use `r.Status().Update` or use Patch
|
||||
- Reading the same resource multiple times in one reconcile → read once
|
||||
- Reconcile body > 80 lines → extract `reconcileXxx` subroutines per phase
|
||||
- HTTP calls without `ctx` → can't cancel during shutdown
|
||||
- No requeue path for transient errors → silent failures
|
||||
- Missing `OwnerReferences` on children → cascading deletion broken
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
# Tooling landscape
|
||||
|
||||
Five mainstream operator frameworks. Pick by language, complexity, and target environment.
|
||||
|
||||
## At-a-glance
|
||||
|
||||
| Framework | Language | Scaffolding | Webhook support | Best for | Project status |
|
||||
|---|---|---|---|---|---|
|
||||
| **controller-runtime** | Go | None (library) | Yes | Production-grade, low-level | Active (sig-api-machinery) |
|
||||
| **kubebuilder** | Go | Yes (CLI) | Yes | Standard Go operator path | Active (Kubernetes SIGs) |
|
||||
| **operator-sdk** | Go / Helm / Ansible | Yes (CLI) | Yes | OpenShift, mixed paradigms | Active (Red Hat) |
|
||||
| **metacontroller** | Any (webhook) | None | N/A (uses webhooks) | Polyglot, avoid Go | Less active |
|
||||
| **KOPF** | Python | None (library) | Yes | Python shops, async-first | Active (community) |
|
||||
| **java-operator-sdk** | Java | Yes | Yes | JVM shops | Active (Red Hat / Java SIG) |
|
||||
|
||||
## Decision tree
|
||||
|
||||
```
|
||||
Primary language?
|
||||
├── Go ──┬── Need scaffolding + opinionated path → kubebuilder
|
||||
│ ├── Targeting OpenShift / OLM → operator-sdk (Go)
|
||||
│ └── Library-only, full control → controller-runtime
|
||||
├── Python ─────────────────────────────────────────→ KOPF
|
||||
├── Java ─────────────────────────────────────────→ java-operator-sdk
|
||||
└── Other (Node, Ruby, Rust)
|
||||
└── webhook-based, polyglot → metacontroller
|
||||
```
|
||||
|
||||
## controller-runtime (Go library)
|
||||
|
||||
**What it is:** The Go library that everyone else builds on. Provides `Manager`, `Reconciler`, cache, client, predicates, leader election.
|
||||
|
||||
**Use when:**
|
||||
- You need fine-grained control over the manager and event sources
|
||||
- You're building reusable operator components
|
||||
- Your team has Go experience and prefers libraries to scaffolders
|
||||
|
||||
**Skip when:**
|
||||
- You want bootstrap-by-CLI (use kubebuilder)
|
||||
- You don't speak Go
|
||||
|
||||
**Example:**
|
||||
```go
|
||||
mgr, _ := ctrl.NewManager(cfg, ctrl.Options{Scheme: scheme})
|
||||
ctrl.NewControllerManagedBy(mgr).
|
||||
For(&apps.MyApp{}).
|
||||
Complete(&MyAppReconciler{Client: mgr.GetClient()})
|
||||
mgr.Start(ctx)
|
||||
```
|
||||
|
||||
## kubebuilder (Go scaffolder)
|
||||
|
||||
**What it is:** The standard scaffolding tool. Wraps controller-runtime with project layout, code generation, and the `kubebuilder` CLI.
|
||||
|
||||
**Use when:**
|
||||
- New Go operator
|
||||
- You want predictable project structure
|
||||
- You'll publish the operator publicly
|
||||
|
||||
**Workflow:**
|
||||
```bash
|
||||
kubebuilder init --domain example.com --repo github.com/org/myapp-operator
|
||||
kubebuilder create api --group apps --version v1alpha1 --kind MyApp
|
||||
make manifests
|
||||
make generate
|
||||
make run
|
||||
```
|
||||
|
||||
**Strengths:** Excellent docs, mature, used by everyone from cert-manager to Crossplane.
|
||||
|
||||
**Weaknesses:** Some teams find the layout opinionated; sometimes hard to escape from.
|
||||
|
||||
## operator-sdk (Red Hat / OpenShift)
|
||||
|
||||
**What it is:** Wraps kubebuilder for Go and adds Helm-based and Ansible-based operators (no Go required).
|
||||
|
||||
**Use when:**
|
||||
- Targeting OpenShift / OLM (Operator Lifecycle Manager)
|
||||
- Building a Helm-based operator from an existing chart
|
||||
- Building an Ansible-based operator from existing playbooks
|
||||
|
||||
**Helm-based operator:**
|
||||
```bash
|
||||
operator-sdk init --plugins=helm --domain example.com --group apps --version v1 --kind MyApp
|
||||
operator-sdk create api --group apps --version v1 --kind MyApp --helm-chart=./mychart
|
||||
```
|
||||
|
||||
The operator's reconcile becomes `helm upgrade --install`. Fast on-ramp; less power.
|
||||
|
||||
**Ansible-based operator:**
|
||||
Similar, but reconcile invokes a playbook. Useful for ops teams already deep in Ansible.
|
||||
|
||||
**Skip when:**
|
||||
- Vanilla k8s target (kubebuilder is more direct)
|
||||
- You want a Go operator without OpenShift coupling
|
||||
|
||||
## metacontroller (webhook-based, language-agnostic)
|
||||
|
||||
**What it is:** Runs in-cluster, watches your CRDs, and POSTs webhook calls to your endpoints with desired-state computations. You implement the logic in any language behind an HTTP endpoint.
|
||||
|
||||
**Use when:**
|
||||
- Polyglot team (Python, Node, Ruby, etc.)
|
||||
- Want to avoid Go and Java
|
||||
- Operator logic is genuinely simple (compute children from parent)
|
||||
|
||||
**Example sync hook:**
|
||||
```python
|
||||
# Python webhook returns desired children given parent + observed
|
||||
def sync(request):
|
||||
parent = request['parent']
|
||||
return {
|
||||
'status': {'phase': 'Ready'},
|
||||
'children': [{'apiVersion': 'apps/v1', 'kind': 'Deployment', ...}],
|
||||
}
|
||||
```
|
||||
|
||||
**Strengths:** No Go required; fast iteration in any language.
|
||||
|
||||
**Weaknesses:** Lower ecosystem activity; not great for complex multi-CRD operators; webhook-based latency.
|
||||
|
||||
## KOPF (Python)
|
||||
|
||||
**What it is:** A Python framework for building operators. Async-first, decorator-based, no scaffolding step.
|
||||
|
||||
**Use when:**
|
||||
- Python shop
|
||||
- Operator logic is moderate complexity
|
||||
- Want fast iteration without recompilation
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
import kopf
|
||||
|
||||
@kopf.on.create('apps.example.com', 'v1alpha1', 'myapps')
|
||||
async def create_fn(spec, name, namespace, logger, **_):
|
||||
logger.info(f"creating MyApp {name}")
|
||||
# ... create children
|
||||
return {'phase': 'Ready'}
|
||||
|
||||
@kopf.on.delete('apps.example.com', 'v1alpha1', 'myapps')
|
||||
async def delete_fn(spec, name, namespace, **_):
|
||||
# cleanup external resources
|
||||
pass
|
||||
```
|
||||
|
||||
**Strengths:**
|
||||
- Async/await native (good for many concurrent reconciles)
|
||||
- No code generation
|
||||
- Good for ML/data teams already in Python
|
||||
|
||||
**Weaknesses:**
|
||||
- Smaller ecosystem than Go
|
||||
- Some features lag controller-runtime (e.g., complex caching)
|
||||
- Python startup cost in the controller pod
|
||||
|
||||
## java-operator-sdk
|
||||
|
||||
**What it is:** Java framework, Quarkus integration, modeled after controller-runtime.
|
||||
|
||||
**Use when:** JVM shop with strong Spring/Quarkus skills.
|
||||
|
||||
**Skip when:** You don't already have a JVM ops setup.
|
||||
|
||||
## Comparison: complexity vs control
|
||||
|
||||
```
|
||||
control ↑
|
||||
│ controller-runtime (full control, library)
|
||||
│ │
|
||||
│ kubebuilder (scaffolded controller-runtime)
|
||||
│ │
|
||||
│ operator-sdk Go (kubebuilder + OLM)
|
||||
│ │
|
||||
│ KOPF (Python decorators)
|
||||
│ │
|
||||
│ java-operator-sdk (JVM)
|
||||
│ │
|
||||
│ operator-sdk Ansible (playbooks)
|
||||
│ │
|
||||
│ operator-sdk Helm (chart-based)
|
||||
│ │
|
||||
│ metacontroller (webhook hooks)
|
||||
↓
|
||||
complexity ↓
|
||||
```
|
||||
|
||||
Higher control = more code, more flexibility. Lower complexity = faster start, less power.
|
||||
|
||||
## Cross-cutting concerns
|
||||
|
||||
Regardless of framework:
|
||||
|
||||
- **Webhooks for validation** — reject bad CRs at admission
|
||||
- **cert-manager** — rotate webhook certs automatically
|
||||
- **Prometheus** — `/metrics` endpoint via controller-runtime's built-in metrics
|
||||
- **OLM** (Operator Lifecycle Manager) — for OperatorHub publishing
|
||||
- **OperatorHub Capability Levels** — see `operator_capability_audit.py`
|
||||
|
||||
## Migration paths
|
||||
|
||||
| From | To | Effort |
|
||||
|---|---|---|
|
||||
| controller-runtime | kubebuilder | Low (kubebuilder uses controller-runtime) |
|
||||
| Helm chart | Helm-based operator-sdk | Low |
|
||||
| Helm chart | Go operator (kubebuilder) | High (rewrite logic in Go) |
|
||||
| KOPF | Go operator | High (language change) |
|
||||
| Any | metacontroller | Medium (move logic behind HTTP) |
|
||||
|
||||
## Selection checklist
|
||||
|
||||
Before committing:
|
||||
- [ ] Identify primary language constraint
|
||||
- [ ] Target environment (vanilla k8s vs OpenShift/OLM)
|
||||
- [ ] Operator complexity: 1 CRD vs many
|
||||
- [ ] Need webhooks?
|
||||
- [ ] Need OLM publishing?
|
||||
- [ ] Build a 1-week proof-of-concept; verify reconcile latency, status update flow, and dev-loop ergonomics
|
||||
134
engineering/skills/kubernetes-operator/scripts/crd_validator.py
Executable file
134
engineering/skills/kubernetes-operator/scripts/crd_validator.py
Executable file
|
|
@ -0,0 +1,134 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Validate a Kubernetes CRD YAML against operator-pattern best practices.
|
||||
|
||||
Checks for status subresource, structural schema, conditions support, printer
|
||||
columns, version policy, and other operator-grade design rules. Stdlib-only —
|
||||
parses YAML via a minimal embedded reader (no PyYAML dependency).
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
CHECKS = [
|
||||
("status_subresource", "Each version must declare subresources.status (otherwise status updates loop spec reconciles)"),
|
||||
("storage_version", "Exactly one version must be storage:true"),
|
||||
("served_version", "At least one version must be served:true"),
|
||||
("schema_present", "Each version must declare schema.openAPIV3Schema"),
|
||||
("schema_typed", "Schema must declare 'type: object' at root (no x-kubernetes-preserve-unknown-fields at root)"),
|
||||
("conditions_array", "Schema should declare a conditions array under status (for metav1.Conditions)"),
|
||||
("printer_columns", "additionalPrinterColumns should include Age and a status indicator"),
|
||||
("scope", "scope should be Namespaced unless cluster-scoped is justified"),
|
||||
("singular_listkind", "names.singular and names.listKind must be declared"),
|
||||
]
|
||||
|
||||
|
||||
def _load_yaml_minimal(path):
|
||||
"""Yield top-level YAML documents from a multi-doc file as text blocks.
|
||||
|
||||
Stdlib-only — splits on '---' separators. We grep relevant fields with
|
||||
regex rather than fully parse. Crude but enough for the structural
|
||||
checks below; a full YAML parser would be the upgrade path."""
|
||||
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
||||
text = f.read()
|
||||
docs = re.split(r"^---\s*$", text, flags=re.MULTILINE)
|
||||
return [d for d in docs if d.strip()]
|
||||
|
||||
|
||||
def _is_crd_doc(doc):
|
||||
return bool(re.search(r"^kind:\s*CustomResourceDefinition\s*$", doc, re.MULTILINE))
|
||||
|
||||
|
||||
def _check_one(doc, path):
|
||||
findings = []
|
||||
has_status_sub = bool(re.search(r"subresources:\s*\n\s*status:\s*\{?\s*\}?", doc))
|
||||
if not has_status_sub:
|
||||
findings.append(("FAIL", "status_subresource", "no subresources.status block found"))
|
||||
storage_count = len(re.findall(r"storage:\s*true\b", doc))
|
||||
if storage_count != 1:
|
||||
findings.append(("FAIL", "storage_version", f"expected exactly 1 storage:true, found {storage_count}"))
|
||||
served_count = len(re.findall(r"served:\s*true\b", doc))
|
||||
if served_count < 1:
|
||||
findings.append(("FAIL", "served_version", "no served:true version"))
|
||||
if "openAPIV3Schema" not in doc:
|
||||
findings.append(("FAIL", "schema_present", "no openAPIV3Schema declared"))
|
||||
if re.search(r"x-kubernetes-preserve-unknown-fields:\s*true", doc):
|
||||
findings.append(("WARN", "schema_typed", "x-kubernetes-preserve-unknown-fields: true present (defeats validation)"))
|
||||
if "conditions" not in doc.lower():
|
||||
findings.append(("WARN", "conditions_array", "no conditions array referenced (Karpathy: declare an explicit shape)"))
|
||||
if "additionalPrinterColumns" not in doc:
|
||||
findings.append(("WARN", "printer_columns", "no additionalPrinterColumns (kubectl get UX is poor)"))
|
||||
elif not re.search(r"name:\s*Age\b", doc):
|
||||
findings.append(("WARN", "printer_columns", "additionalPrinterColumns missing Age column"))
|
||||
if not re.search(r"^\s*scope:\s*\w+", doc, re.MULTILINE):
|
||||
findings.append(("WARN", "scope", "scope not explicitly set"))
|
||||
if not re.search(r"^\s*singular:\s*[\w<]", doc, re.MULTILINE):
|
||||
findings.append(("WARN", "singular_listkind", "names.singular not declared"))
|
||||
if not re.search(r"^\s*listKind:\s*[\w<]", doc, re.MULTILINE):
|
||||
findings.append(("WARN", "singular_listkind", "names.listKind not declared"))
|
||||
return findings
|
||||
|
||||
|
||||
def _walk_yaml_files(root):
|
||||
if os.path.isfile(root):
|
||||
yield root
|
||||
return
|
||||
for r, _, files in os.walk(root):
|
||||
for f in files:
|
||||
if f.endswith((".yaml", ".yml")):
|
||||
yield os.path.join(r, f)
|
||||
|
||||
|
||||
def audit(target):
|
||||
results = []
|
||||
for path in _walk_yaml_files(target):
|
||||
for doc in _load_yaml_minimal(path):
|
||||
if not _is_crd_doc(doc):
|
||||
continue
|
||||
kind_match = re.search(r"kind:\s*(\w+)\s*$", doc, re.MULTILINE)
|
||||
crd_kind = kind_match.group(1) if kind_match else "?"
|
||||
name_match = re.search(r"^\s+name:\s*([\w.\-]+)\s*$", doc, re.MULTILINE)
|
||||
crd_name = name_match.group(1) if name_match else os.path.basename(path)
|
||||
findings = _check_one(doc, path)
|
||||
results.append({"path": path, "name": crd_name, "kind": crd_kind, "findings": findings})
|
||||
return results
|
||||
|
||||
|
||||
def render_text(results):
|
||||
if not results:
|
||||
print("No CRD documents found.")
|
||||
return 0
|
||||
fails = sum(1 for r in results for f in r["findings"] if f[0] == "FAIL")
|
||||
warns = sum(1 for r in results for f in r["findings"] if f[0] == "WARN")
|
||||
print(f"CRD Validator — {len(results)} CRD(s) inspected, {fails} FAIL, {warns} WARN")
|
||||
print("")
|
||||
for r in results:
|
||||
print(f"== {r['name']} ({r['path']})")
|
||||
if not r["findings"]:
|
||||
print(" PASS: all checks green")
|
||||
continue
|
||||
for level, key, msg in r["findings"]:
|
||||
print(f" [{level}] {key}: {msg}")
|
||||
print("")
|
||||
return 1 if fails else 0
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--crd", required=True, help="Path to a CRD YAML file or a directory of YAMLs")
|
||||
ap.add_argument("--format", choices=["text", "json"], default="text")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.exists(args.crd):
|
||||
print(f"ERROR: not found: {args.crd}", file=sys.stderr)
|
||||
return 2
|
||||
results = audit(args.crd)
|
||||
if args.format == "json":
|
||||
print(json.dumps(results, indent=2))
|
||||
return 0
|
||||
return render_text(results)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
150
engineering/skills/kubernetes-operator/scripts/operator_capability_audit.py
Executable file
150
engineering/skills/kubernetes-operator/scripts/operator_capability_audit.py
Executable file
|
|
@ -0,0 +1,150 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Score an operator against OperatorHub Capability Levels (1-5).
|
||||
|
||||
Walks an operator repo and detects evidence for each level. Level achieved =
|
||||
highest level for which all required signals are present. Reports next-level
|
||||
gaps as concrete advancement steps.
|
||||
|
||||
Levels:
|
||||
L1 Basic Install — CRD + controller + Deployment manifest
|
||||
L2 Seamless Upgrades — version conversion + PDB + leader election
|
||||
L3 Full Lifecycle — backup/restore + finalizers + status conditions
|
||||
L4 Deep Insights — /metrics endpoint + Prometheus rules
|
||||
L5 Auto Pilot — HPA / VPA / autotuning logic referenced
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
SIGNALS = {
|
||||
"L1": [
|
||||
("crd_present", lambda files, contents: any("CustomResourceDefinition" in c for c in contents.values())),
|
||||
("deployment_present", lambda files, contents: any(re.search(r"^kind:\s*Deployment", c, re.MULTILINE) for c in contents.values())),
|
||||
("controller_code", lambda files, contents: any(p.endswith(".go") and "Reconcile" in c for p, c in contents.items())),
|
||||
],
|
||||
"L2": [
|
||||
("conversion_webhook", lambda files, contents: any("conversion" in c.lower() and "webhook" in c.lower() for c in contents.values())),
|
||||
("leader_election", lambda files, contents: any("LeaderElection" in c or "leader-elect" in c for c in contents.values())),
|
||||
("pdb_present", lambda files, contents: any(re.search(r"kind:\s*PodDisruptionBudget", c) for c in contents.values())),
|
||||
],
|
||||
"L3": [
|
||||
("finalizers", lambda files, contents: any("Finalizer" in c or "finalizers" in c for c in contents.values())),
|
||||
("status_conditions", lambda files, contents: any("metav1.Condition" in c or "SetStatusCondition" in c for c in contents.values())),
|
||||
("backup_restore_hint", lambda files, contents: any(re.search(r"\b(backup|restore|snapshot)\b", c, re.IGNORECASE) for c in contents.values())),
|
||||
],
|
||||
"L4": [
|
||||
("metrics_endpoint", lambda files, contents: any(re.search(r"/metrics|prometheus", c) for c in contents.values())),
|
||||
("prometheus_rules", lambda files, contents: any(re.search(r"PrometheusRule|alert:", c) for c in contents.values())),
|
||||
],
|
||||
"L5": [
|
||||
("autoscaling_referenced", lambda files, contents: any(re.search(r"\bHorizontalPodAutoscaler|VerticalPodAutoscaler|autoscal", c) for c in contents.values())),
|
||||
("autotune_logic", lambda files, contents: any(re.search(r"autotune|self-heal|anomaly", c, re.IGNORECASE) for c in contents.values())),
|
||||
],
|
||||
}
|
||||
|
||||
LEVEL_NAMES = {
|
||||
"L1": "Basic Install",
|
||||
"L2": "Seamless Upgrades",
|
||||
"L3": "Full Lifecycle",
|
||||
"L4": "Deep Insights",
|
||||
"L5": "Auto Pilot",
|
||||
}
|
||||
|
||||
SCAN_EXTS = {".go", ".yaml", ".yml", ".md"}
|
||||
SKIP_DIRS = {".git", "node_modules", "vendor", "bin", "dist", "__pycache__"}
|
||||
|
||||
|
||||
def _walk(root):
|
||||
files = {}
|
||||
for r, dirs, fnames in os.walk(root):
|
||||
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
|
||||
for f in fnames:
|
||||
if os.path.splitext(f)[1] in SCAN_EXTS:
|
||||
p = os.path.join(r, f)
|
||||
try:
|
||||
with open(p, "r", encoding="utf-8", errors="replace") as fh:
|
||||
files[p] = fh.read()
|
||||
except OSError:
|
||||
continue
|
||||
return files
|
||||
|
||||
|
||||
def evaluate(operator_dir):
|
||||
contents = _walk(operator_dir)
|
||||
file_paths = list(contents.keys())
|
||||
results = {}
|
||||
achieved_max = None
|
||||
for level in ["L1", "L2", "L3", "L4", "L5"]:
|
||||
signals = SIGNALS[level]
|
||||
passing = []
|
||||
failing = []
|
||||
for key, check in signals:
|
||||
ok = check(file_paths, contents)
|
||||
(passing if ok else failing).append(key)
|
||||
all_pass = len(failing) == 0
|
||||
results[level] = {
|
||||
"name": LEVEL_NAMES[level],
|
||||
"passing": passing,
|
||||
"missing": failing,
|
||||
"achieved": all_pass,
|
||||
}
|
||||
if all_pass:
|
||||
achieved_max = level
|
||||
else:
|
||||
break
|
||||
return {"current_level": achieved_max, "details": results}
|
||||
|
||||
|
||||
def render_text(report, operator_dir):
|
||||
print(f"Operator Capability Audit — {operator_dir}")
|
||||
current = report["current_level"]
|
||||
if current is None:
|
||||
print("Current level: BELOW_L1 (no operator structure detected)")
|
||||
else:
|
||||
print(f"Current level: {current} — {LEVEL_NAMES[current]}")
|
||||
print("")
|
||||
for level in ["L1", "L2", "L3", "L4", "L5"]:
|
||||
d = report["details"].get(level)
|
||||
if d is None:
|
||||
continue
|
||||
marker = "✓" if d["achieved"] else "✗"
|
||||
print(f" {marker} {level} {d['name']}: pass={len(d['passing'])} miss={len(d['missing'])}")
|
||||
for k in d["missing"]:
|
||||
print(f" - missing: {k}")
|
||||
print("")
|
||||
next_level = None
|
||||
for lv in ["L1", "L2", "L3", "L4", "L5"]:
|
||||
if lv == current:
|
||||
continue
|
||||
if not report["details"].get(lv, {}).get("achieved"):
|
||||
next_level = lv
|
||||
break
|
||||
if next_level:
|
||||
misses = report["details"][next_level]["missing"]
|
||||
print(f"Next: advance to {next_level} ({LEVEL_NAMES[next_level]}) by addressing:")
|
||||
for k in misses:
|
||||
print(f" - {k}")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--operator-dir", required=True, help="Path to operator repo root")
|
||||
ap.add_argument("--format", choices=["text", "json"], default="text")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.isdir(args.operator_dir):
|
||||
print(f"ERROR: not a directory: {args.operator_dir}", file=sys.stderr)
|
||||
return 2
|
||||
report = evaluate(args.operator_dir)
|
||||
if args.format == "json":
|
||||
print(json.dumps(report, indent=2))
|
||||
else:
|
||||
render_text(report, args.operator_dir)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
177
engineering/skills/kubernetes-operator/scripts/reconcile_lint.py
Executable file
177
engineering/skills/kubernetes-operator/scripts/reconcile_lint.py
Executable file
|
|
@ -0,0 +1,177 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Lint a Go controller reconcile function for operator anti-patterns.
|
||||
|
||||
Detects common operator bugs from static patterns in Go source: blocking calls
|
||||
inside reconcile, spec mutation (instead of status), missing requeue on error,
|
||||
oversized reconcile functions, and missing finalizer/condition handling. Pure
|
||||
regex heuristics; not a Go AST parser, but catches the recurring mistakes.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
CODE_EXTS = {".go"}
|
||||
|
||||
|
||||
CHECKS = [
|
||||
("time_sleep", r"\btime\.Sleep\s*\(", "FAIL", "time.Sleep inside reconcile blocks the work queue. Use ctrl.Result{RequeueAfter: ...}."),
|
||||
("update_spec", r"r\.(?:Client\.)?Update\(\s*ctx\s*,\s*\w+\)", "WARN", "r.Client.Update on the reconciled object likely mutates spec. Use r.Status().Update for status."),
|
||||
("missing_context_in_http", r"http\.(?:Get|Post|Do)\s*\(", "WARN", "HTTP calls without ctx-aware client; cannot cancel during shutdown."),
|
||||
("os_exit", r"\bos\.Exit\s*\(", "FAIL", "os.Exit inside reconcile kills the controller; return an error instead."),
|
||||
("panic_call", r"\bpanic\s*\(", "WARN", "panic inside reconcile crashes the controller; return an error so it requeues."),
|
||||
("log_fatal", r"\blog\.Fatal", "FAIL", "log.Fatal exits the process; return an error instead."),
|
||||
]
|
||||
|
||||
|
||||
def _read(path):
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
||||
return f.read()
|
||||
except OSError:
|
||||
return ""
|
||||
|
||||
|
||||
def _find_reconcile_blocks(src):
|
||||
"""Return list of (start_line, end_line, body) for each Reconcile func."""
|
||||
blocks = []
|
||||
sig = re.compile(r"func\s+\([^)]*\)\s+Reconcile\s*\(", re.MULTILINE)
|
||||
for m in sig.finditer(src):
|
||||
start = m.start()
|
||||
i = src.find("{", m.end())
|
||||
if i < 0:
|
||||
continue
|
||||
depth = 1
|
||||
j = i + 1
|
||||
while j < len(src) and depth > 0:
|
||||
c = src[j]
|
||||
if c == "{":
|
||||
depth += 1
|
||||
elif c == "}":
|
||||
depth -= 1
|
||||
j += 1
|
||||
if depth == 0:
|
||||
body = src[i:j]
|
||||
start_line = src[:start].count("\n") + 1
|
||||
end_line = src[:j].count("\n") + 1
|
||||
blocks.append((start_line, end_line, body))
|
||||
return blocks
|
||||
|
||||
|
||||
def _check_block(body, start_line):
|
||||
findings = []
|
||||
for key, pattern, level, msg in CHECKS:
|
||||
for m in re.finditer(pattern, body):
|
||||
line_offset = body[: m.start()].count("\n")
|
||||
findings.append({
|
||||
"level": level,
|
||||
"key": key,
|
||||
"line": start_line + line_offset,
|
||||
"msg": msg,
|
||||
})
|
||||
body_lines = body.count("\n")
|
||||
if body_lines > 80:
|
||||
findings.append({
|
||||
"level": "WARN",
|
||||
"key": "reconcile_length",
|
||||
"line": start_line,
|
||||
"msg": f"Reconcile body is {body_lines} lines (>80). Extract reconcileXxx subroutines.",
|
||||
})
|
||||
has_finalizer_add = re.search(r"controllerutil\.AddFinalizer\b|finalizers\s*=", body)
|
||||
has_finalizer_remove = re.search(r"controllerutil\.RemoveFinalizer\b", body)
|
||||
if has_finalizer_add and not has_finalizer_remove:
|
||||
findings.append({
|
||||
"level": "WARN",
|
||||
"key": "finalizer_unbalanced",
|
||||
"line": start_line,
|
||||
"msg": "AddFinalizer found but no RemoveFinalizer call — orphaned external resources on delete.",
|
||||
})
|
||||
if not re.search(r"ctrl\.Result\{", body):
|
||||
findings.append({
|
||||
"level": "WARN",
|
||||
"key": "missing_requeue",
|
||||
"line": start_line,
|
||||
"msg": "Reconcile body does not return ctrl.Result{...}. Confirm error returns trigger requeue.",
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def audit_file(path):
|
||||
src = _read(path)
|
||||
if not src or "Reconcile" not in src:
|
||||
return []
|
||||
blocks = _find_reconcile_blocks(src)
|
||||
out = []
|
||||
for start_line, _, body in blocks:
|
||||
out.extend(_check_block(body, start_line))
|
||||
# Cross-function check: AddFinalizer present in file → RemoveFinalizer must be too.
|
||||
has_add = "controllerutil.AddFinalizer" in src or re.search(r"finalizers\s*=", src)
|
||||
has_remove = "controllerutil.RemoveFinalizer" in src
|
||||
if has_add and not has_remove:
|
||||
out = [f for f in out if f["key"] != "finalizer_unbalanced"]
|
||||
out.append({
|
||||
"level": "WARN",
|
||||
"key": "finalizer_unbalanced",
|
||||
"line": 0,
|
||||
"msg": "AddFinalizer is called somewhere in this file but RemoveFinalizer is not — orphaned external resources on delete.",
|
||||
})
|
||||
elif has_remove:
|
||||
# Suppress per-block warnings if file-level pairing is balanced.
|
||||
out = [f for f in out if f["key"] != "finalizer_unbalanced"]
|
||||
return out
|
||||
|
||||
|
||||
def _walk(target):
|
||||
if os.path.isfile(target):
|
||||
yield target
|
||||
return
|
||||
for r, _, files in os.walk(target):
|
||||
for f in files:
|
||||
if os.path.splitext(f)[1] in CODE_EXTS:
|
||||
yield os.path.join(r, f)
|
||||
|
||||
|
||||
def audit(target):
|
||||
results = []
|
||||
for path in _walk(target):
|
||||
findings = audit_file(path)
|
||||
if findings:
|
||||
results.append({"path": path, "findings": findings})
|
||||
return results
|
||||
|
||||
|
||||
def render_text(results):
|
||||
fails = sum(1 for r in results for f in r["findings"] if f["level"] == "FAIL")
|
||||
warns = sum(1 for r in results for f in r["findings"] if f["level"] == "WARN")
|
||||
print(f"Reconcile Lint — {len(results)} controller file(s), {fails} FAIL, {warns} WARN")
|
||||
print("")
|
||||
if not results:
|
||||
print("PASS: no anti-patterns detected.")
|
||||
return 0
|
||||
for r in results:
|
||||
print(f"== {r['path']}")
|
||||
for f in r["findings"]:
|
||||
print(f" [{f['level']}] line {f['line']} {f['key']}: {f['msg']}")
|
||||
print("")
|
||||
return 1 if fails else 0
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--controller", required=True, help="Path to a Go controller file or directory")
|
||||
ap.add_argument("--format", choices=["text", "json"], default="text")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.exists(args.controller):
|
||||
print(f"ERROR: not found: {args.controller}", file=sys.stderr)
|
||||
return 2
|
||||
results = audit(args.controller)
|
||||
if args.format == "json":
|
||||
print(json.dumps(results, indent=2))
|
||||
return 0
|
||||
return render_text(results)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -225,6 +225,7 @@ nav:
|
|||
- "TC Tracker": skills/engineering/tc-tracker.md
|
||||
- "Karpathy Coder": skills/engineering/karpathy-coder.md
|
||||
- "Feature Flags Architect": skills/engineering/feature-flags-architect.md
|
||||
- "Kubernetes Operator": skills/engineering/kubernetes-operator.md
|
||||
- AgentHub:
|
||||
- "AgentHub": skills/engineering/agenthub.md
|
||||
- "/hub:init": skills/engineering/agenthub-init.md
|
||||
|
|
@ -431,3 +432,4 @@ nav:
|
|||
- "/tc": commands/tc.md
|
||||
- "/karpathy-check": commands/karpathy-check.md
|
||||
- "/flag-cleanup": commands/flag-cleanup.md
|
||||
- "/operator-audit": commands/operator-audit.md
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue