<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Owner References Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/owner-references/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/owner-references/</link>
	<description>Infrastructure security, from the kernel up.</description>
	<lastBuildDate>Sat, 09 May 2026 18:40:47 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.3</generator>

<image>
	<url>https://linuxcent.com/wp-content/uploads/2026/04/favicon-512x512-1-150x150.png</url>
	<title>Owner References Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/owner-references/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<item>
		<title>Kubernetes CRDs in Production: Finalizers, Status Conditions, and RBAC Patterns</title>
		<link>https://linuxcent.com/kubernetes-crd-production-finalizers-conditions-rbac/</link>
					<comments>https://linuxcent.com/kubernetes-crd-production-finalizers-conditions-rbac/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Sat, 25 Apr 2026 18:17:16 +0000</pubDate>
				<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[CRD]]></category>
		<category><![CDATA[Finalizers]]></category>
		<category><![CDATA[Owner References]]></category>
		<category><![CDATA[Production Kubernetes]]></category>
		<category><![CDATA[RBAC]]></category>
		<category><![CDATA[Status Conditions]]></category>
		<guid isPermaLink="false">https://linuxcent.com/kubernetes-crd-production-finalizers-conditions-rbac/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 8</span> <span class="rt-label rt-postfix">minutes</span></span>Ship production-ready Kubernetes CRDs: finalizer design patterns, status condition conventions, owner references, RBAC for custom resources, and common failure modes.</p>
<p>The post <a href="https://linuxcent.com/kubernetes-crd-production-finalizers-conditions-rbac/">Kubernetes CRDs in Production: Finalizers, Status Conditions, and RBAC Patterns</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 8</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>Kubernetes CRDs &amp; Operators: Extending the API, Episode 10</em><br />
<em><a href="/what-is-kubernetes-crd/">What Is a CRD?</a> · <a href="/kubernetes-custom-resources-examples/">CRDs You Already Use</a> · <a href="/kubernetes-crd-schema-explained/">CRD Anatomy</a> · <a href="/write-kubernetes-crd-yaml-walkthrough/">Write Your First CRD</a> · <a href="/kubernetes-crd-cel-validation/">CEL Validation</a> · <a href="/kubernetes-controller-reconcile-loop/">Controller Loop</a> · <a href="/build-kubernetes-operator-controller-runtime/">Build an Operator</a> · <a href="/kubernetes-crd-versioning-conversion-webhook/">CRD Versioning</a> · <a href="/kubernetes-admission-webhooks-explained/">Admission Webhooks</a> · </em><em><a href="/kubernetes-crd-production-finalizers-conditions-rbac/">CRDs in Production</a></em></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li><strong>Finalizers</strong> block deletion until cleanup completes — they prevent orphaned external resources but cause stuck objects if the controller crashes mid-cleanup; always implement a removal timeout</li>
<li><strong>Status conditions</strong> are the standard communication channel between controller and user: use <code class="" data-line="">type</code>, <code class="" data-line="">status</code>, <code class="" data-line="">reason</code>, <code class="" data-line="">message</code>, and <code class="" data-line="">observedGeneration</code> on every condition; never invent ad-hoc status fields</li>
<li><strong>Owner references</strong> wire automatic garbage collection — when the parent custom resource is deleted, Kubernetes deletes owned child objects; use them for every object your controller creates in the same namespace</li>
<li><strong>RBAC for CRDs in multi-tenant clusters</strong> must include separate ClusterRoles for controller, editor, and viewer; grant <code class="" data-line="">status</code> and <code class="" data-line="">finalizers</code> as separate sub-resources; never give application teams cluster-scoped create/delete on CRDs</li>
<li>The three most common Kubernetes CRD production failure modes: finalizer death loop, status thrash, and CRD deletion cascade — all avoidable with the patterns in this episode</li>
<li>Running <code class="" data-line="">kubectl get crds</code> on a healthy cluster should show <code class="" data-line="">Established: True</code> for every CRD; non-Established CRDs silently reject all create requests</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">  PRODUCTION CRD LIFECYCLE: FULL PICTURE

  Create         Reconcile        Suspend/Resume      Delete
  ──────         ─────────        ──────────────      ──────
  User applies   Controller       User patches         User deletes
  BackupPolicy   creates CronJob, spec.suspended=true  BackupPolicy
      │          sets status          │                    │
      ▼              │                ▼                    ▼
  Admission      │           Controller          Finalizer blocks
  webhook        │           suspends CronJob     deletion
  (if any)       │                               Controller:
      │          │                                 1. Delete CronJob
      ▼          ▼                                 2. Remove external state
  Schema       Status                              3. Remove finalizer
  validation   conditions                          Object deleted from etcd
      │        updated
      ▼
  Controller
  reconcile
  triggered
</code></pre>
<p>Kubernetes CRD production readiness is not just about making the happy path work — it is about designing for the failure modes: controllers crashing mid-operation, deletion races, and status messages that confuse operators at 2am.</p>
<hr />
<h2 id="finalizers-controlled-deletion">Finalizers: Controlled Deletion</h2>
<p>A finalizer is a string in <code class="" data-line="">metadata.finalizers</code>. Kubernetes will not delete an object that has finalizers, regardless of who issues the delete command.</p>
<pre><code class="" data-line="">metadata:
  name: nightly
  namespace: demo
  finalizers:
    - storage.example.com/backup-cleanup  # ← your controller put this here
</code></pre>
<p>When <code class="" data-line="">kubectl delete bp nightly</code> runs:</p>
<pre><code class="" data-line="">  1. API server sets metadata.deletionTimestamp  (does NOT delete yet)
  2. Object is visible as &quot;Terminating&quot;
  3. Controller sees deletionTimestamp set
  4. Controller runs cleanup:
       - delete backup data from S3
       - delete CronJob (or let owner references handle it)
       - release any external locks
  5. Controller removes the finalizer:
       patch bp nightly --type=json \
         -p &#039;[{&quot;op&quot;:&quot;remove&quot;,&quot;path&quot;:&quot;/metadata/finalizers/0&quot;}]&#039;
  6. API server sees finalizers list is now empty → deletes the object
</code></pre>
<h3 id="adding-a-finalizer-in-go">Adding a finalizer in Go</h3>
<pre><code class="" data-line="">const finalizerName = &quot;storage.example.com/backup-cleanup&quot;

func (r *BackupPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    bp := &amp;storagev1alpha1.BackupPolicy{}
    if err := r.Get(ctx, req.NamespacedName, bp); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // Deletion path
    if !bp.DeletionTimestamp.IsZero() {
        if controllerutil.ContainsFinalizer(bp, finalizerName) {
            if err := r.cleanupExternalResources(ctx, bp); err != nil {
                return ctrl.Result{}, err
            }
            controllerutil.RemoveFinalizer(bp, finalizerName)
            if err := r.Update(ctx, bp); err != nil {
                return ctrl.Result{}, err
            }
        }
        return ctrl.Result{}, nil
    }

    // Normal path: ensure finalizer is present
    if !controllerutil.ContainsFinalizer(bp, finalizerName) {
        controllerutil.AddFinalizer(bp, finalizerName)
        if err := r.Update(ctx, bp); err != nil {
            return ctrl.Result{}, err
        }
    }

    // ... rest of reconcile
}
</code></pre>
<h3 id="finalizer-death-loop-and-the-timeout-pattern">Finalizer death loop and the timeout pattern</h3>
<p>If <code class="" data-line="">cleanupExternalResources</code> always returns an error (external system down, bug in cleanup code), the object gets stuck in <code class="" data-line="">Terminating</code> forever. The operator cannot delete it; <code class="" data-line="">kubectl delete --force</code> does not help with finalizers.</p>
<p>Prevention: add a cleanup deadline with status tracking.</p>
<pre><code class="" data-line="">func (r *BackupPolicyReconciler) cleanupExternalResources(ctx context.Context, bp *storagev1alpha1.BackupPolicy) error {
    // Check if we&#039;ve been trying to clean up for too long
    if bp.DeletionTimestamp != nil {
        deadline := bp.DeletionTimestamp.Add(10 * time.Minute)
        if time.Now().After(deadline) {
            // Log the failure, abandon cleanup, let the object be deleted.
            log.FromContext(ctx).Error(nil, &quot;cleanup deadline exceeded, removing finalizer anyway&quot;,
                &quot;name&quot;, bp.Name)
            return nil   // returning nil removes the finalizer
        }
    }
    // ... actual cleanup
}
</code></pre>
<p>Recovery for a stuck object (use only when cleanup truly cannot succeed):</p>
<pre><code class="" data-line="">kubectl patch bp nightly -n demo --type=json \
  -p &#039;[{&quot;op&quot;:&quot;remove&quot;,&quot;path&quot;:&quot;/metadata/finalizers&quot;}]&#039;
</code></pre>
<hr />
<h2 id="status-conditions-the-right-way">Status Conditions: The Right Way</h2>
<p>The Kubernetes standard condition format is defined in <code class="" data-line="">k8s.io/apimachinery/pkg/apis/meta/v1.Condition</code>:</p>
<pre><code class="" data-line="">type Condition struct {
    Type               string          // e.g. &quot;Ready&quot;, &quot;Synced&quot;, &quot;Degraded&quot;
    Status             ConditionStatus // &quot;True&quot;, &quot;False&quot;, &quot;Unknown&quot;
    ObservedGeneration int64           // the .metadata.generation this condition reflects
    LastTransitionTime metav1.Time     // when Status last changed
    Reason             string          // machine-readable, CamelCase, e.g. &quot;CronJobCreated&quot;
    Message            string          // human-readable, may contain details
}
</code></pre>
<h3 id="standard-condition-types">Standard condition types</h3>
<table>
<thead>
<tr>
<th>Type</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="" data-line="">Ready</code></td>
<td>The resource is fully reconciled and operational</td>
</tr>
<tr>
<td><code class="" data-line="">Synced</code></td>
<td>The resource has been synced with an external system</td>
</tr>
<tr>
<td><code class="" data-line="">Progressing</code></td>
<td>An operation is actively in progress</td>
</tr>
<tr>
<td><code class="" data-line="">Degraded</code></td>
<td>The resource is operating in a reduced capacity</td>
</tr>
</tbody>
</table>
<p>Use <code class="" data-line="">Ready: True</code> only when the full reconcile is complete and the resource is functional. Use <code class="" data-line="">Ready: False</code> with a clear <code class="" data-line="">Message</code> when reconcile fails or is blocked.</p>
<h3 id="setting-conditions-in-go">Setting conditions in Go</h3>
<pre><code class="" data-line="">meta.SetStatusCondition(&amp;bpCopy.Status.Conditions, metav1.Condition{
    Type:               &quot;Ready&quot;,
    Status:             metav1.ConditionFalse,
    ObservedGeneration: bp.Generation,
    Reason:             &quot;CronJobCreateFailed&quot;,
    Message:            fmt.Sprintf(&quot;failed to create CronJob: %v&quot;, err),
})
</code></pre>
<p><code class="" data-line="">meta.SetStatusCondition</code> handles deduplication — it updates an existing condition of the same <code class="" data-line="">Type</code> rather than appending a duplicate.</p>
<h3 id="observedgeneration-is-critical"><code class="" data-line="">observedGeneration</code> is critical</h3>
<pre><code class="" data-line="">metadata.generation      = 5   (increments on every spec change)
status.observedGeneration = 3  (set by controller on each reconcile)

If observedGeneration &lt; generation:
  → controller has not yet reconciled the latest spec change
  → status.conditions reflect an older state
  → do NOT alert based on conditions that lag generation
</code></pre>
<p>Always set <code class="" data-line="">ObservedGeneration: bp.Generation</code> when writing status conditions. Tooling (Argo CD, Flux, kubectl wait) depends on this to know whether status is current.</p>
<h3 id="kubectl-wait-uses-conditions"><code class="" data-line="">kubectl wait</code> uses conditions</h3>
<pre><code class="" data-line=""># Wait until BackupPolicy is Ready
kubectl wait bp/nightly -n demo \
  --for=condition=Ready \
  --timeout=60s
</code></pre>
<p>This works because <code class="" data-line="">kubectl wait</code> reads the <code class="" data-line="">status.conditions</code> array.</p>
<hr />
<h2 id="owner-references-automatic-garbage-collection">Owner References: Automatic Garbage Collection</h2>
<p>Owner references wire a parent-child relationship between Kubernetes objects. When the parent is deleted, Kubernetes garbage-collects all owned children automatically.</p>
<pre><code class="" data-line="">metadata:
  name: nightly-backup       # CronJob
  ownerReferences:
    - apiVersion: storage.example.com/v1alpha1
      kind: BackupPolicy
      name: nightly
      uid: a1b2c3d4-...
      controller: true          # only one owner can be the controller
      blockOwnerDeletion: true  # the GC waits for this owner before deleting child
</code></pre>
<p>Set in Go using <code class="" data-line="">ctrl.SetControllerReference</code>:</p>
<pre><code class="" data-line="">if err := ctrl.SetControllerReference(bp, cronJob, r.Scheme); err != nil {
    return ctrl.Result{}, err
}
</code></pre>
<h3 id="owner-reference-rules">Owner reference rules</h3>
<ul>
<li>Owner and owned object must be in the <strong>same namespace</strong> — cluster-scoped objects cannot own namespaced objects</li>
<li>Only one object can be the <code class="" data-line="">controller: true</code> owner; others can be non-controller owners</li>
<li>Deleting the owner cascades to deleting owned objects — this is garbage collection, not finalizer-based cleanup</li>
</ul>
<p>Without owner references, deleting a BackupPolicy leaves the CronJob as an orphan. This is hard to detect and accumulates over time.</p>
<hr />
<h2 id="rbac-patterns-for-multi-tenant-crd-usage">RBAC Patterns for Multi-Tenant CRD Usage</h2>
<p>A production CRD deployment needs three distinct RBAC roles:</p>
<pre><code class="" data-line=""># 1. Controller role — full access for the operator
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: backuppolicy-controller
rules:
  - apiGroups: [&quot;storage.example.com&quot;]
    resources: [&quot;backuppolicies&quot;]
    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;update&quot;, &quot;patch&quot;]
  - apiGroups: [&quot;storage.example.com&quot;]
    resources: [&quot;backuppolicies/status&quot;]
    verbs: [&quot;get&quot;, &quot;update&quot;, &quot;patch&quot;]
  - apiGroups: [&quot;storage.example.com&quot;]
    resources: [&quot;backuppolicies/finalizers&quot;]
    verbs: [&quot;update&quot;]
  - apiGroups: [&quot;batch&quot;]
    resources: [&quot;cronjobs&quot;]
    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;update&quot;, &quot;patch&quot;, &quot;delete&quot;]
---
# 2. Editor role — for application teams (namespaced binding)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: backuppolicy-editor
rules:
  - apiGroups: [&quot;storage.example.com&quot;]
    resources: [&quot;backuppolicies&quot;]
    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;update&quot;, &quot;patch&quot;, &quot;delete&quot;]
  # No status write — only the controller writes status
  # No finalizers write — prevents deletion blocking by non-controllers
---
# 3. Viewer role — for audit, monitoring
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: backuppolicy-viewer
rules:
  - apiGroups: [&quot;storage.example.com&quot;]
    resources: [&quot;backuppolicies&quot;]
    verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;]
</code></pre>
<p>Bind editor/viewer roles at <strong>namespace</strong> scope, not cluster scope:</p>
<pre><code class="" data-line="">apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: team-alpha-backup-editor
  namespace: team-alpha
subjects:
  - kind: Group
    name: team-alpha
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: backuppolicy-editor
  apiGroup: rbac.authorization.k8s.io
</code></pre>
<p>This pattern gives team-alpha full control over BackupPolicies in their namespace but no access to other namespaces — standard Kubernetes multi-tenancy.</p>
<hr />
<h2 id="the-three-production-failure-modes">The Three Production Failure Modes</h2>
<h3 id="1-finalizer-death-loop">1. Finalizer death loop</h3>
<p><strong>Symptoms:</strong> Object stuck in <code class="" data-line="">Terminating</code> for hours; <code class="" data-line="">kubectl get bp nightly</code> shows <code class="" data-line="">DeletionTimestamp</code> set but object exists.</p>
<p><strong>Cause:</strong> <code class="" data-line="">cleanupExternalResources</code> always returns an error.</p>
<p><strong>Detection:</strong></p>
<pre><code class="" data-line="">kubectl get bp nightly -n demo -o jsonpath=&#039;{.metadata.deletionTimestamp}&#039;
# non-empty = stuck in termination
kubectl describe bp nightly -n demo
# look for repeated reconcile error events
</code></pre>
<p><strong>Fix:</strong> Add cleanup deadline in controller; use <code class="" data-line="">kubectl patch</code> to remove finalizer as last resort.</p>
<h3 id="2-status-thrash">2. Status thrash</h3>
<p><strong>Symptoms:</strong> Controller sets <code class="" data-line="">Ready: True</code>, then <code class="" data-line="">Ready: False</code>, then <code class="" data-line="">Ready: True</code> in a rapid loop. Alert noise, confusing dashboards.</p>
<p><strong>Cause:</strong> Each reconcile compares actual state incorrectly due to cache lag — it sees its own status write as a change, re-reconciles, and flips the status again.</p>
<p><strong>Fix:</strong> Set <code class="" data-line="">ObservedGeneration</code> on every condition. Compare <code class="" data-line="">generation</code> with <code class="" data-line="">observedGeneration</code> before re-reconciling. Use <code class="" data-line="">meta.IsStatusConditionTrue</code> to check current condition before overwriting it with the same value.</p>
<pre><code class="" data-line="">// Only update status if it changed
current := meta.FindStatusCondition(bp.Status.Conditions, &quot;Ready&quot;)
if current == nil || current.Status != desired.Status || current.Reason != desired.Reason {
    meta.SetStatusCondition(&amp;bpCopy.Status.Conditions, desired)
    r.Status().Update(ctx, bpCopy)
}
</code></pre>
<h3 id="3-crd-deletion-cascade">3. CRD deletion cascade</h3>
<p><strong>Symptoms:</strong> A team deletes a CRD for cleanup purposes; all instances across all namespaces disappear silently.</p>
<p><strong>Cause:</strong> <code class="" data-line="">kubectl delete crd backuppolicies.storage.example.com</code> — the API server cascades the deletion to all custom resources of that type.</p>
<p><strong>Prevention:</strong><br />
&#8211; Add a <code class="" data-line="">resourcelock</code> annotation on production CRDs managed by your operator<br />
&#8211; Use GitOps (Argo CD, Flux) to manage CRD installation — a deleted CRD is automatically re-applied from the Git source<br />
&#8211; Back up CRDs and instances with <code class="" data-line="">velero</code> or equivalent before any CRD management operations</p>
<hr />
<h2 id="production-readiness-checklist">Production Readiness Checklist</h2>
<pre><code class="" data-line="">CRD DEFINITION
  □ spec.versions has exactly one storage: true version
  □ Status subresource enabled (subresources.status: {})
  □ additionalPrinterColumns includes Ready column from status.conditions
  □ OpenAPI schema defines required fields and types
  □ CEL rules cover cross-field constraints

CONTROLLER
  □ Owner references set on all child resources
  □ Finalizer logic includes cleanup deadline
  □ Status conditions use standard format with observedGeneration
  □ Reconcile function is idempotent
  □ Not-found errors handled cleanly (return nil, not error)
  □ At least 2 replicas with leader election enabled

RBAC
  □ Three ClusterRoles: controller, editor, viewer
  □ Status and finalizers are separate RBAC sub-resources
  □ Editor/viewer bound at namespace scope, not cluster scope
  □ Controller ServiceAccount has only necessary permissions

OPERATIONS
  □ CRD installed via GitOps or Helm (not manual kubectl apply)
  □ Backup of CRDs and instances included in cluster backup
  □ kubectl get crds shows Established: True for all CRDs
  □ Monitoring for stuck Terminating objects (finalizer deadlock)
  □ Alert on controller reconcile error rate, not just pod health
</code></pre>
<hr />
<h2 id="common-mistakes"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Common Mistakes</h2>
<p><strong>Granting <code class="" data-line="">update</code> on <code class="" data-line="">backuppolicies</code> but not <code class="" data-line="">backuppolicies/status</code> to the controller.</strong> If the controller cannot write status, status updates silently fail. The controller appears to run but status conditions never update. Grant both <code class="" data-line="">backuppolicies</code> (for spec/metadata writes) and <code class="" data-line="">backuppolicies/status</code> (for the status subresource path).</p>
<p><strong>Setting <code class="" data-line="">Ready: True</code> before all owned resources are healthy.</strong> If the controller sets <code class="" data-line="">Ready: True</code> after creating the CronJob but before verifying the CronJob is actually <code class="" data-line="">active</code>, users see a false-positive health signal. Only set <code class="" data-line="">Ready: True</code> when you have confirmed the desired state is actually achieved.</p>
<p><strong>Not setting <code class="" data-line="">observedGeneration</code> on status conditions.</strong> Tools like Argo CD and <code class="" data-line="">kubectl wait --for=condition=Ready</code> will report incorrect health status if <code class="" data-line="">observedGeneration</code> is stale. Always set <code class="" data-line="">ObservedGeneration: obj.Generation</code> in every condition write.</p>
<p><strong>Using <code class="" data-line="">kubectl delete crd</code> in a production cluster without a backup.</strong> This is irreversible. Treat CRDs as production-critical infrastructure — require GitOps review, backup verification, and team approval before any CRD deletion.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="" data-line=""># Check for stuck Terminating objects
kubectl get backuppolicies -A --field-selector metadata.deletionTimestamp!=&#039;&#039;

# Force-remove a stuck finalizer (use only when cleanup is truly impossible)
kubectl patch bp nightly -n demo --type=json \
  -p &#039;[{&quot;op&quot;:&quot;remove&quot;,&quot;path&quot;:&quot;/metadata/finalizers/0&quot;}]&#039;

# Check all CRDs are Established
kubectl get crds -o jsonpath=&#039;{range .items[*]}{.metadata.name} {.status.conditions[?(@.type==&quot;Established&quot;)].status}{&quot;\n&quot;}{end}&#039;

# Watch status conditions update during reconcile
kubectl get bp nightly -n demo -w -o \
  jsonpath=&#039;{.status.conditions[?(@.type==&quot;Ready&quot;)].status} {.status.conditions[?(@.type==&quot;Ready&quot;)].message}{&quot;\n&quot;}&#039;

# Verify owner references are set on child CronJob
kubectl get cronjob nightly-backup -n demo \
  -o jsonpath=&#039;{.metadata.ownerReferences}&#039;

# List all objects owned by a BackupPolicy (by label)
kubectl get all -n demo -l backuppolicy=nightly
</code></pre>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Finalizers block deletion until cleanup completes — always implement a cleanup deadline to prevent permanent stuck objects</li>
<li>Status conditions must use the standard format with <code class="" data-line="">observedGeneration</code> — tooling depends on it for correctness</li>
<li>Owner references enable automatic garbage collection of child resources when the parent is deleted</li>
<li>RBAC needs three roles (controller, editor, viewer) with <code class="" data-line="">status</code> and <code class="" data-line="">finalizers</code> as separate sub-resources</li>
<li>The three production failure modes — finalizer death loop, status thrash, CRD deletion cascade — are all preventable with the patterns covered in this episode</li>
</ul>
<hr />
<h2 id="series-complete">Series Complete</h2>
<p>You now have the full picture of Kubernetes CRDs and Operators: from understanding what a CRD is (<a href="/what-is-kubernetes-crd/">EP01</a>), through real examples (<a href="/kubernetes-custom-resources-examples/">EP02</a>), schema design (<a href="/kubernetes-crd-schema-explained/">EP03</a>), hands-on YAML (<a href="/write-kubernetes-crd-yaml-walkthrough/">EP04</a>), CEL validation (<a href="/kubernetes-crd-cel-validation/">EP05</a>), the controller loop (<a href="/kubernetes-controller-reconcile-loop/">EP06</a>), building an operator (<a href="/build-kubernetes-operator-controller-runtime/">EP07</a>), versioning (<a href="/kubernetes-crd-versioning-conversion-webhook/">EP08</a>), admission webhooks (<a href="/kubernetes-admission-webhooks-explained/">EP09</a>), to production patterns in this episode.</p>
<p>The next series in the Kubernetes learning arc on linuxcent.com covers <strong>Kubernetes Networking Deep Dive</strong> — Services, Ingress, Gateway API, CNI, and eBPF networking. Subscribe below to get it when it launches.</p>
<p>Stay subscribed → <a href="https://linuxcent.com">linuxcent.com</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-crd-production-finalizers-conditions-rbac%2F&amp;linkname=Kubernetes%20CRDs%20in%20Production%3A%20Finalizers%2C%20Status%20Conditions%2C%20and%20RBAC%20Patterns" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-crd-production-finalizers-conditions-rbac%2F&amp;linkname=Kubernetes%20CRDs%20in%20Production%3A%20Finalizers%2C%20Status%20Conditions%2C%20and%20RBAC%20Patterns" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-crd-production-finalizers-conditions-rbac%2F&amp;linkname=Kubernetes%20CRDs%20in%20Production%3A%20Finalizers%2C%20Status%20Conditions%2C%20and%20RBAC%20Patterns" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-crd-production-finalizers-conditions-rbac%2F&amp;linkname=Kubernetes%20CRDs%20in%20Production%3A%20Finalizers%2C%20Status%20Conditions%2C%20and%20RBAC%20Patterns" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-crd-production-finalizers-conditions-rbac%2F&amp;linkname=Kubernetes%20CRDs%20in%20Production%3A%20Finalizers%2C%20Status%20Conditions%2C%20and%20RBAC%20Patterns" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-crd-production-finalizers-conditions-rbac%2F&amp;linkname=Kubernetes%20CRDs%20in%20Production%3A%20Finalizers%2C%20Status%20Conditions%2C%20and%20RBAC%20Patterns" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-crd-production-finalizers-conditions-rbac%2F&amp;linkname=Kubernetes%20CRDs%20in%20Production%3A%20Finalizers%2C%20Status%20Conditions%2C%20and%20RBAC%20Patterns" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fkubernetes-crd-production-finalizers-conditions-rbac%2F&#038;title=Kubernetes%20CRDs%20in%20Production%3A%20Finalizers%2C%20Status%20Conditions%2C%20and%20RBAC%20Patterns" data-a2a-url="https://linuxcent.com/kubernetes-crd-production-finalizers-conditions-rbac/" data-a2a-title="Kubernetes CRDs in Production: Finalizers, Status Conditions, and RBAC Patterns"></a></p><p>The post <a href="https://linuxcent.com/kubernetes-crd-production-finalizers-conditions-rbac/">Kubernetes CRDs in Production: Finalizers, Status Conditions, and RBAC Patterns</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/kubernetes-crd-production-finalizers-conditions-rbac/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1702</post-id>	</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: linuxcent.com @ 2026-08-12 20:16:18 by W3 Total Cache
-->