BookinglyTech News
Infraestructura

Un choque de owner references en Kubernetes obliga a partir un CRD en dos

Dos ServiceClaim reclamaron el mismo Namespace, SetControllerReference lanzó AlreadyOwnedError y el reconcile se detuvo. El arreglo fue separar Tenant de ServiceClaim.

3 min de lecturaDev.to0 vistas

Un equipo aplica un segundo ServiceClaim en el mismo clúster y el reconcile se corta. El Namespace que necesita ya tiene dueño —otro ServiceClaim—, así que SetControllerReference devuelve AlreadyOwnedError y el controlador se detiene ahí. El claim se queda en Pending y nunca llega a Ready. El arreglo no fue parchear nada: fue partir el CRD en dos objetos con granularidades distintas.

Esto pasó en idp-platform-lab, una plataforma interna de ejemplo montada sobre controller-runtime. La ADR-007 ya lo había dejado escrito antes de que existiera un segundo custom resource: las relaciones entre CR se tienen que diseñar, y cuando DatabaseClaim y ServiceClaim convivieran habría que decidir quién referencia a quién y quién se encarga del teardown. Tres ADR después, esa línea venció.

Dónde estaba el riesgo

Un objeto de Kubernetes puede llevar varias owner references, pero por convención solo una es la del controlador, la que dice quién responde por el objeto. SetControllerReference aplica esa convención en el lado del cliente: si ya hay un controller owner y otro intenta reclamarlo, la llamada falla antes de mandar ningún update al API server.

El ServiceClaim original tenía nombre y ámbito de servicio, pero reconciliaba recursos que en realidad son del equipo: el Namespace team-, un RoleBinding y un ResourceQuota compartidos. Lo único realmente por servicio era la Application de ArgoCD. Cuando un equipo aplicó un segundo ServiceClaim, el segundo reconcile pidió el mismo Namespace que el primero ya poseía, chocó con AlreadyOwnedError y se quedó ahí. Los tres pasos siguientes —RoleBinding, cuota y Application— ni se ejecutaron: en el estado del claim aparecen ausentes, no en False.

Tenant y ServiceClaim

El corte separa dos granularidades. Tenant es cluster-scoped, va uno por equipo y posee el Namespace, el RoleBinding y la cuota. Su nombre es el nombre del equipo y no hay un spec.team aparte, así que "un Tenant por equipo" lo garantiza la unicidad de nombres del propio API server en lugar de una lógica de controlador que se puede desincronizar. ServiceClaim se queda con image, replicas y una referencia al Tenant por nombre, y solo posee la Application de ArgoCD. Varios ServiceClaim pueden apuntar al mismo Tenant.

En la costura entre los dos aparece una decisión que merece la pena: ¿qué pasa si un ServiceClaim se aplica antes de que exista su Tenant? Lo obvio es rechazarlo. Aquí es la respuesta equivocada, porque un equipo suele aplicar una carpeta con un Tenant y varios ServiceClaim en un solo lote y el API server no garantiza el orden. Si el ServiceClaim fallara, un bundle válido dependería de la suerte. Así que informa TenantReady=False y espera a que el Tenant esté listo. No satisfecho todavía no es lo mismo que inválido.

Para el borrado hay tres finalizers, porque la recolección de basura por owner references no ordena nada: el del ServiceClaim elimina su Application y espera a que desaparezca; la Application lleva el resources-finalizer.argocd.argoproj.io que ArgoCD usa para hacer prune del Deployment y del Service; y el del Tenant bloquea su borrado mientras queden ServiceClaims y borra el namespace al final. Queda un hueco documentado: kubectl delete tenant --cascade=foreground borra el namespace antes de que corra el finalizer y se salta el orden. El cascade por defecto es el camino soportado.

El código que se rompió no está en el historial del repositorio, que fue squashed; scenarios/03-second-service reconstruye el caso y marca qué partes son reconstrucción.

El fondo del asunto sirve para cualquiera que diseñe CRD que comparten recursos entre equipos: la pregunta no es si va a aparecer un conflicto de propiedad, sino cuándo. Vale más decidir el corte antes, como se hizo aquí, que descubrirlo con el claim atascado en Pending.