用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/leroyguillaume/claude --skill kubernetes-operator-conventions命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
基于 SOC 职业分类
| name | kubernetes-operator-conventions |
| description | Kubernetes operator / controller conventions (error handling, |
These rules govern any reconcile path: the handler itself and everything it
calls (clients, token providers, DB access). The mechanics below are written
for kopf (this repo's framework); the same principles map onto
controller-runtime (return ctrl.Result{Requeue: true}, err) and Operator SDK.
This is the non-negotiable core rule.
kopf.TemporaryError (which logs, posts an
event, and re-schedules) and never kopf.PermanentError — a permanent
error stops the retries, which violates this rule. A bare raise of an
arbitrary exception also requeues, but prefer TemporaryError so you control
the message and delay.reason / response message, not just an HTTP status code,
and never the raw stack trace as the only signal.kubectl describe. Do not assume the framework does this for free on a
raised error. In kopf specifically, settings.posting.enabled = True
only enables explicit kopf.event(...) calls — a raised
TemporaryError posts no event unless settings.posting.loggers is
also on (it defaults to False). Post the event explicitly (e.g. a
ProvisioningFailed Warning from a reconcile wrapper). See the
kopf-conventions skill for the full posting model and the gotcha.5xx, 429, network/timeout): retry quickly (e.g. 30s).4xx such as 422 invalid manifest, 403
RBAC; or a failed spec validation): retry slowly (e.g. 300s) to avoid a
hot loop, since they need an external change — but still retry.errors.py) exposing a requeue()
chokepoint plus raise_for_* helpers that extract the cause, log it, and
requeue. Route every reconcile-path failure through it instead of scattering
bare raise_for_status() / re-raises. Extract the shared helper as soon as a
second call site appears in a third module (rule of three across the
operator's I/O surfaces).Anti-patterns to fix on sight: raise kopf.PermanentError; re-raising a raw
ApiException / httpx.HTTPStatusError out of a handler; logging only
status without the reason; swallowing an error and returning so the
resource silently stops reconciling.
409) on create and not-found (404) on
delete as success, not failure — log and continue.404/already-gone is success).When generating CRD YAML (e.g. a crd CLI subcommand driven by the Pydantic /
typed spec models), emit one file per kind into an output directory, not a
single --kind selector or one concatenated file. The generator owns the
filename↔manifest mapping in a single place (e.g. an all_crds() registry next
to the builders) so the CLI stays a thin caller and the pre-commit hook is one
entry, not one-per-kind.
Name each file after the kind in kebab-case, acronym-aware — never glue the words together:
Tenant → tenant.yamlTenantMCPServer → tenant-mcp-server.yaml (not tenantmcpserver.yaml)Kebab conversion needs two passes: split lower/digit→upper boundaries
((?<=[a-z0-9])(?=[A-Z])), then acronym→word boundaries
((?<=[A-Z])(?=[A-Z][a-z]), the P→Server split in MCPServer), then
lowercase. A naive .lower() of the PascalCase kind glues acronyms and is wrong.
Default the command with no output dir to a multi-document YAML stream on
stdout (all kinds, ----separated). A single pre-commit hook regenerates the
whole crds/ directory and the files: regex covers the spec modules plus
crds/.*\.yaml, so the committed CRDs can never drift from the models.
type: "null"A Kubernetes structural schema rejects both type: "null" and an anyOf
node that lacks its own type. Pydantic emits an optional T | None field as
anyOf: [{type: T}, {type: "null"}], which the apiserver refuses on kubectl apply of the CRD. When converting a spec model's JSON Schema to the CRD's
OpenAPI v3 subset, collapse <type> | null unions into the single branch with
nullable: true (carrying over sibling keywords like description). The same
converter must strip additionalProperties: false wherever it sits next to
properties (Pydantic's extra="forbid" leaks it, and structural schemas
forbid the pair). Add a test that walks the generated CRD asserting no node has
type: "null" nor additionalProperties: false beside properties.
Every write to the API (server-side apply, JSON/merge/strategic-merge patch,
create, update) must set an explicit, stable field_manager equal to the
operator's name (e.g. tenant-operator). Never let the client default it:
the apiserver will then attribute the fields to a generic or empty manager,
which is invisible to downstream tooling.
This is non-negotiable because anything that reads .metadata.managedFields
relies on the manager name being recognisable:
managedFieldsManagers (and Flux's
equivalent) tells the GitOps engine "this manager owns these fields, don't
flag them as drift". The lookup is by exact manager name. If the
operator's writes don't carry a recognisable manager, the GitOps engine
cannot defer ownership and the resource stays permanently OutOfSync.kubectl get ... -o yaml
shows nameless entries) and force_conflicts decisions become guesswork.kubectl get ... --show-managed-fields becomes the
canonical answer to "who last wrote this field?". A blank or generic
manager defeats the audit.Apply this uniformly: the SSA call, every patch helper (json-patch+json,
merge-patch+json, strategic-merge-patch+json), and any direct
create/update. Centralise the constant (FIELD_MANAGER = "<operator>")
in one module so every I/O call site uses the same string. A patch helper
that silently omits field_manager is a bug to fix on sight — even if the
write succeeds, it breaks the contract with GitOps and audit tooling
downstream.
The manager appears in managedFields only after a successful write to
the resource — creation by another controller is not enough. When debugging
"my manager doesn't show up", force at least one reconcile that actually
writes, then re-check.
LOG_LEVEL), never a
bespoke flag. Emit debug logs around every external call (inputs + outcome)
and at each decision branch, so a failure is diagnosable from logs alone.kubectl describe, not the operator's pod
logs.text, not jsontext, never json. A human
running kubectl logs reads key=value far more easily than one JSON object
per line. JSON is opt-in for clusters with a log pipeline that parses it.LOG_FORMAT (default text) and keep the binary's default
and the chart's default in sync — don't ship a chart whose logging.format
default disagrees with what the operator does on its own.README.md (a
Troubleshooting section): where to look (events + logs), and the
requeue-always behaviour with the per-class delays.