gw-10 step 01 — A CRD and a level-triggered reconcile loop
Goal
Define a CRD, write a controller-runtime reconciler that converges
actual → desired state idempotently, and report status. This is the
operator pattern in its smallest correct form.
Scaffold (kubebuilder)
mkdir gw10-operator && cd gw10-operator
kubebuilder init --domain 10xdev.io --repo github.com/10xdev/gw10
kubebuilder create api --group net --version v1 --kind EdgeRoute \
--resource --controller
Code — the CRD type (api/v1/edgeroute_types.go)
package v1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// EdgeRouteSpec is the DESIRED state (what the user writes).
type EdgeRouteSpec struct {
Hostname string `json:"hostname"`
Prefix string `json:"prefix"`
Service string `json:"service"` // target k8s Service name
Port int32 `json:"port"`
}
// EdgeRouteStatus is CONTROLLER-written outcome (the receipt).
type EdgeRouteStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
type EdgeRoute struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec EdgeRouteSpec `json:"spec,omitempty"`
Status EdgeRouteStatus `json:"status,omitempty"`
}
Code — the reconciler (internal/controller/edgeroute_controller.go)
package controller
import (
"context"
netv1 "github.com/10xdev/gw10/api/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/api/errors"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type EdgeRouteReconciler struct {
client.Client
Programmer DataPlane // pushes config to the proxy (step 02 / gw-08)
}
// Reconcile is LEVEL-TRIGGERED: it derives the right state from scratch
// every call. It must be idempotent and survive being called any number
// of times in any order.
func (r *EdgeRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var er netv1.EdgeRoute
if err := r.Get(ctx, req.NamespacedName, &er); err != nil {
// NotFound = deleted. Level-triggered: nothing to do here; the
// data plane is reconciled from the full set of EdgeRoutes.
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 1. Observe ACTUAL state: does the target Service exist / have endpoints?
var svc corev1.Service
svcErr := r.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: er.Spec.Service}, &svc)
if errors.IsNotFound(svcErr) {
r.setCondition(ctx, &er, "Accepted", metav1.ConditionFalse,
"ServiceNotFound", "target Service does not exist")
return ctrl.Result{}, nil
}
// 2. Converge: program the data plane for THIS route (idempotent).
if err := r.Programmer.Program(ctx, &er); err != nil {
r.setCondition(ctx, &er, "Programmed", metav1.ConditionFalse,
"PushFailed", err.Error()) // the CRD analog of an xDS NACK
return ctrl.Result{Requeue: true}, nil
}
// 3. Report status: only Programmed=true AFTER the push succeeded.
r.setCondition(ctx, &er, "Accepted", metav1.ConditionTrue, "Valid", "route accepted")
r.setCondition(ctx, &er, "Programmed", metav1.ConditionTrue, "Synced", "data plane updated")
return ctrl.Result{}, nil
}
func (r *EdgeRouteReconciler) setCondition(ctx context.Context, er *netv1.EdgeRoute,
t string, s metav1.ConditionStatus, reason, msg string) {
meta.SetStatusCondition(&er.Status.Conditions, metav1.Condition{
Type: t, Status: s, Reason: reason, Message: msg,
ObservedGeneration: er.Generation,
})
_ = r.Status().Update(ctx, er)
}
// SetupWithManager wires the watch: reconcile on EdgeRoute changes AND
// on changes to the Services they reference (so endpoint churn re-triggers).
func (r *EdgeRouteReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&netv1.EdgeRoute{}).
Owns(&corev1.Service{}).
Complete(r)
}
Tasks
- Scaffold, define the CRD, install it (
make install), run the controller (make run). kubectl applyanEdgeRoutepointing at a missing Service; confirmstatus.conditionsshowsAccepted=False / ServiceNotFound. Create the Service; confirm it flips toAccepted=True / Programmed=Truewithout you touching the EdgeRoute — that's level-triggered self-healing.- Kill and restart the controller; confirm it reconciles all existing EdgeRoutes back to correct status on startup (no events needed).
Acceptance
- A working CRD + reconciler that reports accurate status conditions.
- Demonstrated self-healing: fixing the underlying Service flips status with no change to the CR.
- Controller restart reconverges from scratch (proves level-triggered).
Discussion prompts
- Why must
Reconcilebe idempotent, and what breaks if it isn't? - Why report status after the data-plane push, not before? (Status must reflect reality — the xDS ACK/NACK analogy.)
- Why does watching the referenced
Servicematter? (Endpoint churn in gw-09 must re-trigger reconcile so the data plane stays current.)