Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions docs/guide/usage/crud-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,25 +184,35 @@ The `delete()` method accepts optional parameters for fine-grained control:
public function delete(
array $query = ['pretty' => 1],
?int $gracePeriod = null,
string $propagationPolicy = 'Foreground'
PropagationPolicy|string $propagationPolicy = PropagationPolicy::FOREGROUND
): bool
```

Example with options:

```php
use RenokiCo\PhpK8s\Enums\PropagationPolicy;

// Delete with 30-second grace period
$pod->delete(
query: ['pretty' => 1],
gracePeriod: 30,
propagationPolicy: 'Foreground'
propagationPolicy: PropagationPolicy::FOREGROUND
);
```

**Propagation Policies:**
- `Foreground` - Waits for dependents to be deleted first
- `Background` - Deletes immediately, dependents deleted in background
- `Orphan` - Leaves dependents orphaned
- `PropagationPolicy::FOREGROUND` - Waits for dependents to be deleted first
- `PropagationPolicy::BACKGROUND` - Deletes immediately, dependents deleted in background
- `PropagationPolicy::ORPHAN` - Leaves dependents orphaned

The equivalent strings (`'Foreground'`, `'Background'`, `'Orphan'`) are still
accepted; anything else raises an `InvalidArgumentException` before the request
is sent, rather than being rejected by the API.

Under `Foreground`, the resource stays readable until its dependents are gone,
so a `get()` immediately after `delete()` returns the resource with a
`deletionTimestamp` instead of a 404.

## Creating or Updating Resources

Expand Down
13 changes: 13 additions & 0 deletions src/Enums/PropagationPolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace RenokiCo\PhpK8s\Enums;

/**
* Dependent deletion policies accepted by the Kubernetes DeleteOptions API.
*/
enum PropagationPolicy: string
{
case ORPHAN = 'Orphan';
case BACKGROUND = 'Background';
case FOREGROUND = 'Foreground';
}
62 changes: 51 additions & 11 deletions src/Traits/RunsClusterOperations.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
namespace RenokiCo\PhpK8s\Traits;

use Closure;
use InvalidArgumentException;
use RenokiCo\PhpK8s\Contracts\Attachable;
use RenokiCo\PhpK8s\Contracts\Executable;
use RenokiCo\PhpK8s\Contracts\Loggable;
use RenokiCo\PhpK8s\Contracts\Scalable;
use RenokiCo\PhpK8s\Contracts\Watchable;
use RenokiCo\PhpK8s\Enums\Operation;
use RenokiCo\PhpK8s\Enums\PropagationPolicy;
use RenokiCo\PhpK8s\Exceptions\KubernetesAPIException;
use RenokiCo\PhpK8s\Exceptions\KubernetesAttachException;
use RenokiCo\PhpK8s\Exceptions\KubernetesExecException;
Expand Down Expand Up @@ -233,31 +235,27 @@ public function update(array $query = ['pretty' => 1]): bool
/**
* Delete the resource.
*
* @param null|int $gracePeriod
*
* @throws KubernetesAPIException
*/
public function delete(array $query = ['pretty' => 1], $gracePeriod = null, string $propagationPolicy = 'Foreground'): bool
public function delete(array $query = ['pretty' => 1], ?int $gracePeriod = null, PropagationPolicy|string $propagationPolicy = PropagationPolicy::FOREGROUND): bool
{
if (! $this->isSynced()) {
return true;
}

$this->setAttribute('preconditions', [
'resourceVersion' => $this->getResourceVersion(),
'uid' => $this->getResourceUid(),
'propagationPolicy' => $propagationPolicy,
'gracePeriodSeconds' => $gracePeriod,
]);

$this->refresh();

$payload = json_encode(
$this->deleteOptions($gracePeriod, $propagationPolicy),
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
);

$this->cluster
->setResourceClass(get_class($this))
->runOperation(
Operation::DELETE,
$this->resourcePath(),
$this->toJsonPayload('DeleteOptions'),
$payload,
$query
);

Expand All @@ -266,6 +264,48 @@ public function delete(array $query = ['pretty' => 1], $gracePeriod = null, stri
return true;
}

/**
* Build the DeleteOptions payload for the delete operation.
* The propagationPolicy and gracePeriodSeconds fields are top-level
* DeleteOptions fields; only the resource uid belongs in preconditions.
*
* @return array<string, mixed>
*
* @throws InvalidArgumentException
*/
public function deleteOptions(?int $gracePeriod = null, PropagationPolicy|string $propagationPolicy = PropagationPolicy::FOREGROUND): array
{
$policy = $propagationPolicy instanceof PropagationPolicy
? $propagationPolicy
: PropagationPolicy::tryFrom($propagationPolicy);

if (is_null($policy)) {
throw new InvalidArgumentException(sprintf(
'Invalid propagation policy "%s"; expected one of %s',
$propagationPolicy,
implode(', ', array_column(PropagationPolicy::cases(), 'value'))
));
}

$options = [
'apiVersion' => 'v1',
'kind' => 'DeleteOptions',
'propagationPolicy' => $policy->value,
];

if (! is_null($gracePeriod)) {
$options['gracePeriodSeconds'] = $gracePeriod;
}

if (! is_null($uid = $this->getResourceUid())) {
$options['preconditions'] = [
'uid' => $uid,
];
}

return $options;
}

/**
* Apply the resource using server-side apply.
*
Expand Down
2 changes: 2 additions & 0 deletions tests/CSIDriverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ public function runDeletionTests()

$this->assertTrue($csiDriver->delete());

$this->waitUntilDeleted($csiDriver);

$this->expectException(KubernetesAPIException::class);

$this->cluster->getCSIDriverByName('test-csi-driver.example.com');
Expand Down
156 changes: 156 additions & 0 deletions tests/DeleteOptionsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<?php

namespace RenokiCo\PhpK8s\Test;

use InvalidArgumentException;
use RenokiCo\PhpK8s\Enums\PropagationPolicy;

class DeleteOptionsTest extends TestCase
{
public function test_propagation_policy_is_a_top_level_delete_options_field()
{
$job = $this->cluster->job()->setName('reap');

$job->syncWith([
'metadata' => [
'name' => 'reap',
'uid' => 'aa11bb22-cc33-dd44-ee55-ff6677889900',
'resourceVersion' => '12345',
],
]);

$options = $job->deleteOptions(null, 'Foreground');

$this->assertSame('DeleteOptions', $options['kind']);
$this->assertSame('v1', $options['apiVersion']);
$this->assertSame('Foreground', $options['propagationPolicy']);
$this->assertSame(['uid' => 'aa11bb22-cc33-dd44-ee55-ff6677889900'], $options['preconditions']);
$this->assertArrayNotHasKey('propagationPolicy', $options['preconditions']);
$this->assertArrayNotHasKey('gracePeriodSeconds', $options['preconditions']);
$this->assertArrayNotHasKey('gracePeriodSeconds', $options);
}

public function test_grace_period_is_a_top_level_delete_options_field()
{
$job = $this->cluster->job()->setName('reap');
Comment thread
abnegate marked this conversation as resolved.

$job->syncWith([
'metadata' => [
'name' => 'reap',
'uid' => 'aa11bb22-cc33-dd44-ee55-ff6677889900',
],
]);

$options = $job->deleteOptions(30, 'Background');

$this->assertSame('Background', $options['propagationPolicy']);
$this->assertSame(30, $options['gracePeriodSeconds']);
$this->assertSame(['uid' => 'aa11bb22-cc33-dd44-ee55-ff6677889900'], $options['preconditions']);
}

public function test_preconditions_are_omitted_without_a_resource_uid()
{
$job = $this->cluster->job()->setName('reap');

$options = $job->deleteOptions(null, 'Foreground');

$this->assertSame('Foreground', $options['propagationPolicy']);
$this->assertArrayNotHasKey('preconditions', $options);
}

public function test_invalid_propagation_policy_is_rejected_before_the_request()
{
$job = $this->cluster->job()->setName('reap');

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid propagation policy "foreground"');

$job->deleteOptions(null, 'foreground');
}

public function test_propagation_policy_accepts_the_enum()
{
$job = $this->cluster->job()->setName('reap');

$this->assertSame(
'Orphan',
$job->deleteOptions(null, PropagationPolicy::ORPHAN)['propagationPolicy']
);
}

public function test_delete_sends_the_propagation_policy_at_the_top_level()
{
$cluster = (new RecordingCluster('http://127.0.0.1:8080'))
->respondWith([
['kind' => 'Job', 'apiVersion' => 'batch/v1', 'metadata' => [
'name' => 'reap',
'uid' => 'aa11bb22-cc33-dd44-ee55-ff6677889900',
]],
['kind' => 'Status', 'apiVersion' => 'v1', 'status' => 'Success'],
]);

$job = $cluster->job()->setName('reap');
$job->syncWith([
'metadata' => [
'name' => 'reap',
'uid' => 'aa11bb22-cc33-dd44-ee55-ff6677889900',
],
]);

$this->assertTrue($job->delete(gracePeriod: 30, propagationPolicy: PropagationPolicy::BACKGROUND));

$requests = $cluster->requests();

$this->assertCount(2, $requests, 'delete() refreshes the resource, then deletes it');
$this->assertSame('GET', $requests[0]->getMethod());

$delete = $requests[1];
$this->assertSame('DELETE', $delete->getMethod());

$sent = json_decode((string) $delete->getBody(), true);

$this->assertSame('Background', $sent['propagationPolicy'] ?? null);
$this->assertSame(30, $sent['gracePeriodSeconds'] ?? null);
$this->assertSame(
['uid' => 'aa11bb22-cc33-dd44-ee55-ff6677889900'],
$sent['preconditions'] ?? null,
'only the uid belongs in preconditions'
);
}

public function test_delete_options_payload_shape_matches_the_kubernetes_api()
{
$job = $this->cluster->job()->setName('reap');

$job->syncWith([
'metadata' => [
'name' => 'reap',
'uid' => 'aa11bb22-cc33-dd44-ee55-ff6677889900',
],
'spec' => [
'template' => [
'spec' => [
'containers' => [],
],
],
],
]);

$payload = $job->deleteOptions(null, 'Foreground');

$this->assertSame(
[
'apiVersion' => 'v1',
'kind' => 'DeleteOptions',
'propagationPolicy' => 'Foreground',
'preconditions' => [
'uid' => 'aa11bb22-cc33-dd44-ee55-ff6677889900',
],
],
$payload
);

$this->assertArrayNotHasKey('spec', $payload);
$this->assertArrayNotHasKey('metadata', $payload);
}
}
2 changes: 2 additions & 0 deletions tests/IngressTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ public function runDeletionTests()

$this->assertTrue($ingress->delete());

$this->waitUntilDeleted($ingress);

$this->expectException(KubernetesAPIException::class);

$this->cluster->getIngressByName('nginx');
Expand Down
Loading
Loading