Skip to content

Finalizers ​

Finalizers allow resources to perform cleanup operations before deletion. They're essential for building Kubernetes operators and controllers that manage dependent resources.

What are Finalizers? ​

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:

  1. Kubernetes sets metadata.deletionTimestamp
  2. The resource enters "Terminating" state
  3. Controllers remove their finalizers after cleanup
  4. Once all finalizers are removed, the resource is deleted

Managing Finalizers ​

Get Finalizers ​

php
$configMap = $cluster->getConfigMapByName('my-config', 'default');

$finalizers = $configMap->getFinalizers();
// Returns: ['example.com/cleanup', 'example.com/backup']

Set Finalizers ​

php
$configMap->setFinalizers([
    'example.com/cleanup',
    'example.com/backup',
]);

Add a Finalizer ​

The addFinalizer() method is idempotent - adding the same finalizer twice has no effect:

php
$configMap->addFinalizer('example.com/cleanup');

// Safe to call multiple times
$configMap->addFinalizer('example.com/cleanup');

Remove a Finalizer ​

php
$configMap->removeFinalizer('example.com/cleanup');

Check for a Finalizer ​

php
if ($configMap->hasFinalizer('example.com/cleanup')) {
    echo "Cleanup finalizer is present";
}

Operator Pattern Example ​

Here's a complete example of using finalizers in an operator:

php
use RenokiCo\PhpK8s\KubernetesCluster;

$cluster = new KubernetesCluster('http://127.0.0.1:8080');
$finalizerName = 'example.com/database-backup';

// When creating a resource
$configMap = $cluster->configMap()
    ->setName('database-config')
    ->setNamespace('production')
    ->setData(['connection' => 'postgresql://...'])
    ->addFinalizer($finalizerName)
    ->create();

// Later, in your reconciliation loop...
$configMap = $cluster->getConfigMapByName('database-config', 'production');

if ($configMap->getAttribute('metadata.deletionTimestamp')) {
    // Resource is being deleted
    echo "Performing cleanup before deletion...\n";

    // Do your cleanup (backup database, etc.)
    performDatabaseBackup($configMap);

    // Remove finalizer to allow deletion
    // IMPORTANT: Use jsonMergePatch, not update(), on resources being deleted
    $configMap->jsonMergePatch([
        'metadata' => [
            'finalizers' => array_values(
                array_filter(
                    $configMap->getFinalizers(),
                    fn($f) => $f !== $finalizerName
                )
            ),
        ],
    ]);

    echo "Cleanup complete, resource will be deleted\n";
} else {
    // Normal reconciliation
    echo "Resource is active\n";
}

Best Practices ​

Finalizer Naming ​

Use domain-prefixed names to avoid conflicts:

php
// Good
$pod->addFinalizer('mycompany.com/cleanup');
$pod->addFinalizer('myoperator.io/backup');

// Avoid
$pod->addFinalizer('cleanup');  // Too generic

Removing Finalizers During Deletion ​

When a resource is being deleted (has deletionTimestamp), you cannot use update(). Use jsonMergePatch() instead:

php
// ❌ WRONG - will fail with 400 Bad Request
$resource->removeFinalizer('my-finalizer')->update();

// ✅ CORRECT - use patch operations
$resource->jsonMergePatch([
    'metadata' => [
        'finalizers' => [],  // Or array without your finalizer
    ],
]);

Idempotent Cleanup ​

Make your cleanup operations idempotent - they should be safe to run multiple times:

php
function performCleanup($resource) {
    $backupId = $resource->getLabel('backup-id');

    if ($backupId && backupExists($backupId)) {
        deleteBackup($backupId);
    }

    // Safe to call even if backup doesn't exist
}

Timeout Protection ​

Add timeouts to prevent stuck resources:

php
$deletionTime = strtotime($configMap->getAttribute('metadata.deletionTimestamp'));
$gracePeriod = 300; // 5 minutes

if (time() - $deletionTime > $gracePeriod) {
    // Force remove finalizer after grace period
    $configMap->jsonMergePatch([
        'metadata' => ['finalizers' => []],
    ]);
}

Common Use Cases ​

Resource Dependency Management ​

php
// Parent resource manages child lifecycle
$parent = $cluster->configMap()
    ->setName('parent-config')
    ->addFinalizer('example.com/delete-children')
    ->create();

// On deletion, clean up children
if ($parent->getAttribute('metadata.deletionTimestamp')) {
    $children = $cluster->getAllConfigMaps()->filter(function ($cm) use ($parent) {
        return $cm->getLabel('parent') === $parent->getName();
    });

    foreach ($children as $child) {
        $child->delete();
    }

    $parent->removeFinalizer('example.com/delete-children');
    $parent->jsonMergePatch([
        'metadata' => ['finalizers' => $parent->getFinalizers()],
    ]);
}

External Resource Cleanup ​

php
// Clean up external resources (S3 buckets, databases, etc.)
$backup = $cluster->configMap()
    ->setName('backup-config')
    ->addFinalizer('example.com/s3-cleanup')
    ->create();

if ($backup->getAttribute('metadata.deletionTimestamp')) {
    $bucketName = $backup->getData('s3-bucket');

    // Delete S3 bucket
    $s3Client->deleteBucket(['Bucket' => $bucketName]);

    // Remove finalizer
    $backup->jsonMergePatch([
        'metadata' => ['finalizers' => []],
    ]);
}

Documentation for cuppett/php-k8s fork

Released under the Apache-2.0 License. Originally from renoki-co/php-k8s.