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
13 changes: 2 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,20 +240,11 @@ Run command:
Use number of days or 0 as show to remove expiration completely. By default, it's dry-run. Override with `-f` parameter.

### Purge deleted projects
Purge already deleted projects (remove residual metadata, optionally ignoring backend errors) using a Manage API token and a CSV piped via STDIN.
Purge already deleted projects (remove residual metadata, optionally ignoring backend errors) using a Manage API token.

```
cat deleted-projects.csv | php cli.php storage:deleted-projects-purge [--ignore-backend-errors] <manageToken>
php cli.php storage:deleted-projects-purge [--ignore-backend-errors] <manageToken> <stackUrl> [--ignore-backend-errors] <projectIds>
```
Input CSV header must be exactly:
```
id,name
```
Behavior:
- Validates header.
- For each row calls Manage API purgeDeletedProject; prints command execution id.
- Polls every second (max 600s) until project `isPurged` is true; errors on timeout.
- With --ignore-backend-errors it instructs API to ignore backend failures and just purge metadata (buckets/workspaces records).

### Set data retention for multiple projects
Set data retention days for specific projects listed in a CSV piped via STDIN.
Expand Down
98 changes: 60 additions & 38 deletions src/Keboola/Console/Command/DeletedProjectsPurge.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<?php

namespace Keboola\Console\Command;

use Keboola\ManageApi\Client;
use Keboola\ManageApi\ClientException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
Expand All @@ -10,73 +12,85 @@

class DeletedProjectsPurge extends Command
{

protected function configure()
{
$this
->setName('storage:deleted-projects-purge')
->setDescription('Purge deleted projects.')
->addArgument('url', InputArgument::REQUIRED, 'URL of stack including https://')
->addArgument('token', InputArgument::REQUIRED, 'manage api token')
->addArgument('projectIds', InputArgument::REQUIRED, 'IDs of projects to purge (separate multiple IDs with a space)')
->addOption('ignore-backend-errors', null, InputOption::VALUE_NONE, "Ignore errors from backend and just delete buckets and workspaces metadata")
;
->addOption('force', null, InputOption::VALUE_NONE, 'Actually perform destructive operations (purge). Without this flag, the command will only simulate actions.');
}


protected function execute(InputInterface $input, OutputInterface $output)
{
$url = $input->getArgument('url');
$token = $input->getArgument('token');

$fh = fopen('php://stdin', 'r');
if (!$fh) {
throw new \Exception('Error on input read');
}

$projectIds = $input->getArgument('projectIds');
$ignoreBackendErrors = (bool) $input->getOption('ignore-backend-errors');
$force = (bool) $input->getOption('force');

$output->writeln(sprintf(
'Ignore backend errors %s',
$ignoreBackendErrors ? 'On' : 'Off'
));
$output->writeln(sprintf(
'Force mode %s',
$force ? 'On (destructive operations will be performed)' : 'Off (no destructive operations will be performed)'
));

$client = new Client([
'url' => $url,
'token' => $token,
]);
$projectIds = array_filter(explode(',', $projectIds), 'is_numeric');

$lineNumber = 0;
while ($row = fgetcsv($fh)) {
if ($lineNumber === 0) {
$this->validateHeader($row);
} else {
$this->purgeProject(
$client,
$output,
$ignoreBackendErrors,
$row[0],
$row[1]
);
}
$lineNumber++;
foreach ($projectIds as $projectId) {
$this->purgeProject(
$client,
$output,
$ignoreBackendErrors,
(int) $projectId,
$force,
);
}
}

private function validateHeader($header)
{
$expectedHeader = ['id', 'name'];
if ($header !== $expectedHeader) {
throw new \Exception(sprintf(
'Invalid input header: %s Expected header: %s',
implode(',', $header),
implode(',', $expectedHeader)
));
private function purgeProject(
Client $client,
OutputInterface $output,
bool $ignoreBackendErrors,
int $projectId,
bool $force,
): void {
try {
$deletedProject = $client->getDeletedProject($projectId);
if ($deletedProject['isPurged'] === true) {
$output->writeln(sprintf('<info>INFO</info> Project "%d" purged already.', $projectId));
return;
}
} catch (ClientException $e) {
if ($e->getCode() === 404) {
$output->writeln(sprintf('<error>Error</error>: Purge of the project "%d" not found.', $projectId));

return;
}
$output->writeln(sprintf('<error>Error</error>: Purge of the project "%d" is not possible due "%s".', $projectId, $e->getMessage()));
return;
}
}

private function purgeProject(Client $client, OutputInterface $output, $ignoreBackendErrors, $projectId, $projectName)
{
$projectName = $deletedProject['name'] ?? 'unknown';
$output->writeln(sprintf('Purge %s (%d)', $projectName, $projectId));

if (!$force) {
$output->writeln("[DRY-RUN] Would purge project $projectId");
return;
}

$response = $client->purgeDeletedProject($projectId, [
'ignoreBackendErrors' => (bool) $ignoreBackendErrors,
'ignoreBackendErrors' => $ignoreBackendErrors,
]);
$output->writeln(" - execution id {$response['commandExecutionId']}");

Expand All @@ -87,9 +101,17 @@ private function purgeProject(Client $client, OutputInterface $output, $ignoreBa
if (time() - $startTime > $maxWaitTimeSeconds) {
throw new \Exception("Project {$projectId} purge timeout.");
}
sleep(1);
sleep(2);
$output->writeln(
sprintf(
' - - Waiting for project "%s" (%s) to be purged: execution id %s',
$projectName,
$projectId,
$response['commandExecutionId'],
)
);
} while ($deletedProject['isPurged'] !== true);

$output->writeln(sprintf('Purge done %s (%d)', $projectName, $projectId));
$output->writeln(sprintf('<info>Purge done "%s" (%d)</info>', $projectName, $projectId));
}
}