|
| 1 | +# Finalizers |
| 2 | + |
| 3 | +Finalizers allow resources to perform cleanup operations before deletion. They're essential for building Kubernetes operators and controllers that manage dependent resources. |
| 4 | + |
| 5 | +## What are Finalizers? |
| 6 | + |
| 7 | +Finalizers are strings in `metadata.finalizers` that prevent a resource from being fully deleted until all finalizers are removed. When a resource with finalizers is deleted: |
| 8 | + |
| 9 | +1. Kubernetes sets `metadata.deletionTimestamp` |
| 10 | +2. The resource enters "Terminating" state |
| 11 | +3. Controllers remove their finalizers after cleanup |
| 12 | +4. Once all finalizers are removed, the resource is deleted |
| 13 | + |
| 14 | +## Managing Finalizers |
| 15 | + |
| 16 | +### Get Finalizers |
| 17 | + |
| 18 | +```php |
| 19 | +$configMap = $cluster->getConfigMapByName('my-config', 'default'); |
| 20 | + |
| 21 | +$finalizers = $configMap->getFinalizers(); |
| 22 | +// Returns: ['example.com/cleanup', 'example.com/backup'] |
| 23 | +``` |
| 24 | + |
| 25 | +### Set Finalizers |
| 26 | + |
| 27 | +```php |
| 28 | +$configMap->setFinalizers([ |
| 29 | + 'example.com/cleanup', |
| 30 | + 'example.com/backup', |
| 31 | +]); |
| 32 | +``` |
| 33 | + |
| 34 | +### Add a Finalizer |
| 35 | + |
| 36 | +The `addFinalizer()` method is idempotent - adding the same finalizer twice has no effect: |
| 37 | + |
| 38 | +```php |
| 39 | +$configMap->addFinalizer('example.com/cleanup'); |
| 40 | + |
| 41 | +// Safe to call multiple times |
| 42 | +$configMap->addFinalizer('example.com/cleanup'); |
| 43 | +``` |
| 44 | + |
| 45 | +### Remove a Finalizer |
| 46 | + |
| 47 | +```php |
| 48 | +$configMap->removeFinalizer('example.com/cleanup'); |
| 49 | +``` |
| 50 | + |
| 51 | +### Check for a Finalizer |
| 52 | + |
| 53 | +```php |
| 54 | +if ($configMap->hasFinalizer('example.com/cleanup')) { |
| 55 | + echo "Cleanup finalizer is present"; |
| 56 | +} |
| 57 | +``` |
| 58 | + |
| 59 | +## Operator Pattern Example |
| 60 | + |
| 61 | +Here's a complete example of using finalizers in an operator: |
| 62 | + |
| 63 | +```php |
| 64 | +use RenokiCo\PhpK8s\KubernetesCluster; |
| 65 | + |
| 66 | +$cluster = new KubernetesCluster('http://127.0.0.1:8080'); |
| 67 | +$finalizerName = 'example.com/database-backup'; |
| 68 | + |
| 69 | +// When creating a resource |
| 70 | +$configMap = $cluster->configMap() |
| 71 | + ->setName('database-config') |
| 72 | + ->setNamespace('production') |
| 73 | + ->setData(['connection' => 'postgresql://...']) |
| 74 | + ->addFinalizer($finalizerName) |
| 75 | + ->create(); |
| 76 | + |
| 77 | +// Later, in your reconciliation loop... |
| 78 | +$configMap = $cluster->getConfigMapByName('database-config', 'production'); |
| 79 | + |
| 80 | +if ($configMap->getAttribute('metadata.deletionTimestamp')) { |
| 81 | + // Resource is being deleted |
| 82 | + echo "Performing cleanup before deletion...\n"; |
| 83 | + |
| 84 | + // Do your cleanup (backup database, etc.) |
| 85 | + performDatabaseBackup($configMap); |
| 86 | + |
| 87 | + // Remove finalizer to allow deletion |
| 88 | + // IMPORTANT: Use jsonMergePatch, not update(), on resources being deleted |
| 89 | + $configMap->jsonMergePatch([ |
| 90 | + 'metadata' => [ |
| 91 | + 'finalizers' => array_values( |
| 92 | + array_filter( |
| 93 | + $configMap->getFinalizers(), |
| 94 | + fn($f) => $f !== $finalizerName |
| 95 | + ) |
| 96 | + ), |
| 97 | + ], |
| 98 | + ]); |
| 99 | + |
| 100 | + echo "Cleanup complete, resource will be deleted\n"; |
| 101 | +} else { |
| 102 | + // Normal reconciliation |
| 103 | + echo "Resource is active\n"; |
| 104 | +} |
| 105 | +``` |
| 106 | + |
| 107 | +## Best Practices |
| 108 | + |
| 109 | +### Finalizer Naming |
| 110 | + |
| 111 | +Use domain-prefixed names to avoid conflicts: |
| 112 | + |
| 113 | +```php |
| 114 | +// Good |
| 115 | +$pod->addFinalizer('mycompany.com/cleanup'); |
| 116 | +$pod->addFinalizer('myoperator.io/backup'); |
| 117 | + |
| 118 | +// Avoid |
| 119 | +$pod->addFinalizer('cleanup'); // Too generic |
| 120 | +``` |
| 121 | + |
| 122 | +### Removing Finalizers During Deletion |
| 123 | + |
| 124 | +When a resource is being deleted (has `deletionTimestamp`), you **cannot** use `update()`. Use `jsonMergePatch()` instead: |
| 125 | + |
| 126 | +```php |
| 127 | +// ❌ WRONG - will fail with 400 Bad Request |
| 128 | +$resource->removeFinalizer('my-finalizer')->update(); |
| 129 | + |
| 130 | +// ✅ CORRECT - use patch operations |
| 131 | +$resource->jsonMergePatch([ |
| 132 | + 'metadata' => [ |
| 133 | + 'finalizers' => [], // Or array without your finalizer |
| 134 | + ], |
| 135 | +]); |
| 136 | +``` |
| 137 | + |
| 138 | +### Idempotent Cleanup |
| 139 | + |
| 140 | +Make your cleanup operations idempotent - they should be safe to run multiple times: |
| 141 | + |
| 142 | +```php |
| 143 | +function performCleanup($resource) { |
| 144 | + $backupId = $resource->getLabel('backup-id'); |
| 145 | + |
| 146 | + if ($backupId && backupExists($backupId)) { |
| 147 | + deleteBackup($backupId); |
| 148 | + } |
| 149 | + |
| 150 | + // Safe to call even if backup doesn't exist |
| 151 | +} |
| 152 | +``` |
| 153 | + |
| 154 | +### Timeout Protection |
| 155 | + |
| 156 | +Add timeouts to prevent stuck resources: |
| 157 | + |
| 158 | +```php |
| 159 | +$deletionTime = strtotime($configMap->getAttribute('metadata.deletionTimestamp')); |
| 160 | +$gracePeriod = 300; // 5 minutes |
| 161 | + |
| 162 | +if (time() - $deletionTime > $gracePeriod) { |
| 163 | + // Force remove finalizer after grace period |
| 164 | + $configMap->jsonMergePatch([ |
| 165 | + 'metadata' => ['finalizers' => []], |
| 166 | + ]); |
| 167 | +} |
| 168 | +``` |
| 169 | + |
| 170 | +## Common Use Cases |
| 171 | + |
| 172 | +### Resource Dependency Management |
| 173 | + |
| 174 | +```php |
| 175 | +// Parent resource manages child lifecycle |
| 176 | +$parent = $cluster->configMap() |
| 177 | + ->setName('parent-config') |
| 178 | + ->addFinalizer('example.com/delete-children') |
| 179 | + ->create(); |
| 180 | + |
| 181 | +// On deletion, clean up children |
| 182 | +if ($parent->getAttribute('metadata.deletionTimestamp')) { |
| 183 | + $children = $cluster->getAllConfigMaps()->filter(function ($cm) use ($parent) { |
| 184 | + return $cm->getLabel('parent') === $parent->getName(); |
| 185 | + }); |
| 186 | + |
| 187 | + foreach ($children as $child) { |
| 188 | + $child->delete(); |
| 189 | + } |
| 190 | + |
| 191 | + $parent->removeFinalizer('example.com/delete-children'); |
| 192 | + $parent->jsonMergePatch([ |
| 193 | + 'metadata' => ['finalizers' => $parent->getFinalizers()], |
| 194 | + ]); |
| 195 | +} |
| 196 | +``` |
| 197 | + |
| 198 | +### External Resource Cleanup |
| 199 | + |
| 200 | +```php |
| 201 | +// Clean up external resources (S3 buckets, databases, etc.) |
| 202 | +$backup = $cluster->configMap() |
| 203 | + ->setName('backup-config') |
| 204 | + ->addFinalizer('example.com/s3-cleanup') |
| 205 | + ->create(); |
| 206 | + |
| 207 | +if ($backup->getAttribute('metadata.deletionTimestamp')) { |
| 208 | + $bucketName = $backup->getData('s3-bucket'); |
| 209 | + |
| 210 | + // Delete S3 bucket |
| 211 | + $s3Client->deleteBucket(['Bucket' => $bucketName]); |
| 212 | + |
| 213 | + // Remove finalizer |
| 214 | + $backup->jsonMergePatch([ |
| 215 | + 'metadata' => ['finalizers' => []], |
| 216 | + ]); |
| 217 | +} |
| 218 | +``` |
| 219 | + |
| 220 | +--- |
| 221 | + |
| 222 | +*Documentation for cuppett/php-k8s fork* |
0 commit comments