Kubernetes Operator Scaffolder
by @charlie-morrison
Scaffold production-ready Kubernetes operators — generate CRDs, controllers, RBAC, webhooks, and Dockerfiles with best practices for Go or Python operators.
clawhub install kubernetes-operator-scaffolder📖 About This Skill
name: kubernetes-operator-scaffolder description: Scaffold production-ready Kubernetes operators — generate CRDs, controllers, RBAC, webhooks, and Dockerfiles with best practices for Go or Python operators. metadata: tags: ["kubernetes", "operator", "crd", "controller", "k8s", "go", "python", "rbac", "devops"]
Kubernetes Operator Scaffolder
Generate a complete, production-ready Kubernetes operator project from a high-level resource description. Produces Custom Resource Definitions (CRDs), reconciliation controllers, RBAC manifests, admission webhooks, Dockerfiles, and CI scaffolding — following the Operator Framework and controller-runtime best practices so you skip weeks of boilerplate.
Use when: "scaffold a kubernetes operator", "create a CRD and controller", "generate operator boilerplate", "build a k8s operator for X", or when you need to extend the Kubernetes API with custom resources.
Prerequisites
Before scaffolding, the agent checks for:
# Go operator (kubebuilder path)
go version # Go 1.22+
kubebuilder version # kubebuilder 4.x
controller-gen --version
kustomize versionPython operator (kopf path)
python3 --version # 3.11+
pip show kopf # kopf framework
pip show kubernetes # k8s client
If tools are missing, the agent provides install commands before proceeding.
Usage
Provide the following inputs:
Database, CacheCluster, MLPipeline)infra.example.com)v1alpha1 for new operatorsgo (kubebuilder) or python (kopf)Example invocation:
> Scaffold a Go operator for a PostgresCluster resource in the db.example.com group. Spec fields: replicas (int, default 3), version (string, default "16"), storageSize (string, default "10Gi"). On create, it should provision a StatefulSet with PVCs. On delete, clean up PVCs.
How It Works
Step 1: Project Structure Generation
Create the full directory tree:
operator-name/
├── api/
│ └── v1alpha1/
│ ├── types.go # CRD Go types with markers
│ ├── groupversion_info.go # scheme registration
│ └── zz_generated.deepcopy.go
├── cmd/
│ └── main.go # entrypoint with manager setup
├── internal/
│ └── controller/
│ ├── reconciler.go # main reconcile loop
│ ├── reconciler_test.go # envtest-based tests
│ └── finalizer.go # cleanup logic
├── config/
│ ├── crd/
│ │ ├── kustomization.yaml
│ │ └── bases/
│ │ └── resource_crd.yaml # generated CRD manifest
│ ├── rbac/
│ │ ├── role.yaml # ClusterRole
│ │ ├── role_binding.yaml # ClusterRoleBinding
│ │ ├── service_account.yaml
│ │ └── kustomization.yaml
│ ├── manager/
│ │ ├── manager.yaml # Deployment
│ │ └── kustomization.yaml
│ ├── webhook/ # if webhooks requested
│ │ ├── manifests.yaml
│ │ └── kustomization.yaml
│ └── default/
│ └── kustomization.yaml # ties everything together
├── hack/
│ └── boilerplate.go.txt
├── Dockerfile
├── Makefile
├── go.mod
├── go.sum
├── PROJECT # kubebuilder project metadata
└── README.md
For Python (kopf) operators, the structure mirrors this with src/handlers.py (kopf decorators), src/resources.py (resource builders), deploy/ (CRD + RBAC + Deployment + kustomize), tests/, Dockerfile, Makefile, and pyproject.toml.
Step 2: CRD Definition
Generate the Custom Resource Definition with:
metav1.Condition standard (Type, Status, Reason, Message, LastTransitionTime)kubectl get shows useful information at a glancepg for PostgresCluster)kubectl get allExample CRD type definition (Go):
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Replicas",type=integer,JSONPath=.spec.replicas
// +kubebuilder:printcolumn:name="Version",type=string,JSONPath=.spec.version
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=.status.phase
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=.metadata.creationTimestamp
// +kubebuilder:resource:shortName=pg;pgc
type PostgresCluster struct {
metav1.TypeMeta json:",inline"
metav1.ObjectMeta json:"metadata,omitempty"
Spec PostgresClusterSpec json:"spec,omitempty"
Status PostgresClusterStatus json:"status,omitempty"
}type PostgresClusterSpec struct {
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=10
// +kubebuilder:default=3
Replicas int32 json:"replicas,omitempty"
// +kubebuilder:validation:Pattern=^\d+$
// +kubebuilder:default="16"
Version string json:"version,omitempty"
// +kubebuilder:default="10Gi"
StorageSize string json:"storageSize,omitempty"
}
type PostgresClusterStatus struct {
Phase string json:"phase,omitempty"
ReadyReplicas int32 json:"readyReplicas,omitempty"
Conditions []metav1.Condition json:"conditions,omitempty"
}
Step 3: Controller / Reconciler
Generate the reconciliation loop with these patterns:
Idempotent reconciliation — every reconcile call converges toward the desired state without side effects on repeated runs:
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx) // 1. Fetch the custom resource
var cluster dbv1alpha1.PostgresCluster
if err := r.Get(ctx, req.NamespacedName, &cluster); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 2. Handle deletion with finalizers
if !cluster.DeletionTimestamp.IsZero() {
return r.handleDeletion(ctx, &cluster)
}
if err := r.ensureFinalizer(ctx, &cluster); err != nil {
return ctrl.Result{}, err
}
// 3. Reconcile owned resources (create-or-update pattern)
if err := r.reconcileStatefulSet(ctx, &cluster); err != nil {
return ctrl.Result{}, err
}
if err := r.reconcileService(ctx, &cluster); err != nil {
return ctrl.Result{}, err
}
// 4. Update status
if err := r.updateStatus(ctx, &cluster); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}
Key patterns included:
meta.SetStatusCondition following KEP-1623Step 4: RBAC Generation
Generate least-privilege RBAC from the controller's actual API calls:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: operator-manager-role
rules:
# Custom resource
- apiGroups: ["db.example.com"]
resources: ["postgresclusters"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["db.example.com"]
resources: ["postgresclusters/status"]
verbs: ["get", "update", "patch"]
- apiGroups: ["db.example.com"]
resources: ["postgresclusters/finalizers"]
verbs: ["update"]
# Owned resources
- apiGroups: ["apps"]
resources: ["statefulsets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["services", "configmaps", "persistentvolumeclaims"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Events
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]
The agent reviews each verb and resource group, removing anything the controller doesn't actually need.
Step 5: Dockerfile and Build
Generate a multi-stage Dockerfile:
FROM golang:1.22 AS builder
ARG TARGETOS TARGETARCH
WORKDIR /workspace
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} \
go build -a -o manager cmd/main.goFROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532
ENTRYPOINT ["/manager"]
Step 6: Testing Scaffold
Generate test files using envtest (Go) or pytest with a fake k8s client (Python). Tests cover: CR creation triggers child resource creation with correct spec, spec updates propagate to child resources, deletion triggers finalizer cleanup, status conditions are set correctly, and error cases requeue with backoff.
Step 7: Makefile
Generate a Makefile with standard targets: manifests (CRD generation), generate (deepcopy), test (envtest), build, docker-build, install (CRDs into cluster), and deploy (full operator deployment via kustomize).
Output
The agent produces:
1. Complete project directory — ready to go build / pip install and docker build
2. CRD YAML — with full OpenAPI schema, ready to kubectl apply
3. RBAC manifests — least-privilege ClusterRole, ClusterRoleBinding, ServiceAccount
4. Controller code — idempotent reconciler with finalizers, status updates, event recording
5. Test scaffold — envtest or pytest setup with example test cases
6. Dockerfile — multi-stage, distroless, non-root
7. Makefile — standard build, test, deploy targets
8. Sample CR — an example custom resource YAML for users to try
Best Practices Enforced
logr / structlog, no fmt.PrintlnSupported Operator Patterns
The agent recognizes and scaffolds these patterns: level-triggered reconciliation (desired state convergence), finalizer-based cleanup for external resources, status aggregation from child resources, config drift detection and correction, dependent resource ordering (e.g., Service after StatefulSet), and external resource management (cloud APIs, DNS).
💡 Examples
Provide the following inputs:
Database, CacheCluster, MLPipeline)infra.example.com)v1alpha1 for new operatorsgo (kubebuilder) or python (kopf)Example invocation:
> Scaffold a Go operator for a PostgresCluster resource in the db.example.com group. Spec fields: replicas (int, default 3), version (string, default "16"), storageSize (string, default "10Gi"). On create, it should provision a StatefulSet with PVCs. On delete, clean up PVCs.
⚙️ Configuration
Before scaffolding, the agent checks for:
# Go operator (kubebuilder path)
go version # Go 1.22+
kubebuilder version # kubebuilder 4.x
controller-gen --version
kustomize versionPython operator (kopf path)
python3 --version # 3.11+
pip show kopf # kopf framework
pip show kubernetes # k8s client
If tools are missing, the agent provides install commands before proceeding.