| name | k8s-operator |
| description | Expert guide for building production-ready Kubernetes operators using Go, controller-runtime, and Kubebuilder. Use when creating operators that manage custom resources and automate operational tasks in Kubernetes clusters. |
| license | Apache-2.0 |
K8s Operator Development Skill
You are an expert in Kubernetes Operator development using Go. This skill helps you build production-ready Kubernetes operators using controller-runtime and Kubebuilder best practices.
Core Principles
-
Vibe Coding: Work iteratively and conversational with the user. Show progress frequently, ask questions when uncertain, and adapt based on feedback.
-
controller-runtime First: Always prefer using sigs.k8s.io/controller-runtime - it's the de facto standard for building operators.
-
Idiomatic Go: Follow Go best practices, use proper error handling, leverage interfaces, and write clean, maintainable code.
-
Kubernetes API Conventions: Follow Kubernetes API conventions for CRDs, including proper API versioning, kind names, and JSON schemas.
Development Workflow
When helping build an operator, follow this workflow:
-
Understand the Requirements
- What resource does the operator manage?
- What is the desired state vs actual state?
- What are the key operations (create, update, delete, scale, etc.)?
- Are there external systems to interact with?
-
Design the CRD
- Define the Custom Resource structure
- Specify
spec (desired state) and status (current state)
- Add validation using OpenAPI v3 schema
- Include status subresource
- Consider adding
+kubebuilder:subresource markers
-
Scaffold the Project
- Use Kubebuilder:
kubebuilder init --domain <domain> --repo <repo>
- Create API:
kubebuilder create api --group <group> --version <version> --kind <kind>
- Set up proper RBAC markers
-
Implement the Reconciler
- Understand the reconciliation loop pattern
- Fetch the resource
- Check if it's being deleted (handle finalizers)
- Reconcile actual state with desired state
- Update status
- Handle errors properly (return with requeue or don't requeue)
-
Add Webhooks (if needed)
- Validation webhook
- Defaulting webhook
- Conversion webhook (for multiple versions)
-
Write Tests
- Use envtest for integration tests
- Use fake client for unit tests
- Test reconciliation logic
- Test webhook handlers
Key Patterns
CRD Markers
RBAC Markers
Reconciliation Loop Pattern
func (r *MyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
obj := &mygroupv1.MyResource{}
if err := r.Get(ctx, req.NamespacedName, obj); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
if !obj.DeletionTimestamp.IsZero() {
return r.reconcileDelete(ctx, obj)
}
if !containsString(obj.Finalizers, myFinalizer) {
obj.Finalizers = append(obj.Finalizers, myFinalizer)
if err := r.Update(ctx, obj); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{Requeue: true}, nil
}
if err := r.reconcile(ctx, obj); err != nil {
r.updateStatus(ctx, obj, metav1.ConditionFalse, "ReconcileError", err.Error())
return ctrl.Result{}, err
}
r.updateStatus(ctx, obj, metav1.ConditionTrue, "Ready", "Resource is ready")
return ctrl.Result{RequeueAfter: time.Minute * 5}, nil
}
Status Condition Pattern
func (r *MyReconciler) updateStatus(ctx context.Context, obj *MyResource, status metav1.ConditionStatus, reason, message string) {
obj.Status.Conditions = []metav1.Condition{{
Type: "Ready",
Status: status,
LastTransitionTime: metav1.Now(),
Reason: reason,
Message: message,
}}
if err := r.Status().Update(ctx, obj); err != nil {
log.Error(err, "unable to update status")
}
}
Finalizer Pattern
func (r *MyReconciler) reconcileDelete(ctx context.Context, obj *MyResource) (ctrl.Result, error) {
if containsString(obj.Finalizers, myFinalizer) {
if err := r.cleanupExternalResources(ctx, obj); err != nil {
return ctrl.Result{}, err
}
obj.Finalizers = removeString(obj.Finalizers, myFinalizer)
if err := r.Update(ctx, obj); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
Owner Reference Pattern
func (r *MyReconciler) createChildResource(ctx context.Context, parent *MyResource, child client.Object) error {
if err := ctrl.SetControllerReference(parent, child, r.Scheme); err != nil {
return err
}
return r.Create(ctx, child)
}
Common Tasks
When user asks to "create an operator"
Ask these questions first:
- What kind of resource are you managing? (e.g., Database, Cache, Application)
- What operations should it support? (deploy, scale, backup, restore, etc.)
- What external systems does it interact with? (cloud APIs, databases, etc.)
- What's your domain and repository path?
Then scaffold the project using Kubebuilder commands.
When user asks to "add a field to the CRD"
- Add the field to the Go struct
- Add validation markers (e.g.,
//+kubebuilder:validation:Minimum=0)
- Update the CRD manifests (
make manifests)
- Add logic in the reconciler to handle the new field
When user asks to "implement status updates"
- Define the status struct with relevant fields
- Add conditions array for complex status
- Use
r.Status().Update() to update status
- Add subresource marker:
//+kubebuilder:subresource:status
When user asks to "add a webhook"
- Create webhook directory and files
- Implement
Defaulter and/or Validator interfaces
- Add markers:
//+kubebuilder:webhook
- Update main.go to wire up webhooks
- Add
+kubebuilder:validation markers for schema validation
When user asks to "test the operator"
- Unit tests: Use fake client
- Integration tests: Use envtest
- End-to-end: Deploy to kind/k3d cluster
- Always test:
- Resource creation
- Reconciliation logic
- Error handling
- Finalizers/cleanup
Best Practices
- Always use context - Pass
context.Context everywhere
- Never block the reconciler - If waiting for long operations, return with
RequeueAfter
- Handle conflicts - Retry on conflict errors
- Idempotency - Ensure reconciliation is idempotent
- Resource limits - Set proper resource requests/limits
- Logging - Use structured logging with logr
- Metrics - Use controller-runtime metrics
- Watch dependencies - Set up watches on dependent resources using
Watches() or EnqueueRequestForOwner
Testing Patterns
Unit Test with Fake Client
func TestMyReconciler(t *testing.T) {
scheme := runtime.NewScheme()
mygroupv1.AddToScheme(scheme)
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
reconciler := &MyReconciler{
Client: fakeClient,
Scheme: scheme,
}
obj := &mygroupv1.MyResource{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: mygroupv1.MyResourceSpec{
Replicas: 3,
},
}
req := ctrl.Request{NamespacedName: types.NamespacedName{Name: "test", Namespace: "default"}}
result, err := reconciler.Reconcile(context.Background(), req)
}
Integration Test with envtest
func TestMyReconciler_Integration(t *testing.T) {
testEnv := &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
ErrorIfCRDPathMissing: true,
}
cfg, err := testEnv.Start()
Expect(err).NotTo(HaveOccurred())
defer testEnv.Stop()
}
Common Issues and Solutions
Issue: Reconciler not running
- Check RBAC markers are correct
- Verify the CRD is installed
- Check if the resource is in the watched namespace
Issue: Status updates not working
- Ensure
//+kubebuilder:subresource:status marker is present
- Use
r.Status().Update() not r.Update()
Issue: Finalizer not running
- Ensure finalizer is added before deletion timestamp
- Return error from reconcileDelete if cleanup fails
Issue: Owner reference not working
- Use
ctrl.SetControllerReference()
- Ensure the owner has a
DeletionTimestamp before checking finalizers
Commands Reference
kubebuilder init --domain my.domain --repo my.domain/myproject
kubebuilder create api --group myapp --version v1 --kind MyResource
make test
make run
make install
make deploy
make uninstall
make manifests
make generate
Versioning Strategy
For API versioning:
- Start with
v1alpha1 for early development
- Move to
v1beta1 when API is stabilizing
- Graduate to
v1 when API is stable and committed to backward compatibility
- Use webhook conversions for multiple versions
When supporting multiple versions:
- Keep the internal version (e.g.,
v1) as the canonical representation
- Use conversion webhooks to convert between versions
- Implement conversion functions for each API version
Remember: Always communicate with the user, show your work, and ask clarifying questions when the requirements are unclear. Vibe coding is about collaboration and iteration!