From 8a62442adee5b6a793f2e2d8e8aa9c7b8ed51aba Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Tue, 29 Jul 2025 15:33:58 +0200 Subject: [PATCH 1/5] command to delete backends # Conflicts: # cli.php --- cli.php | 2 + .../Console/Command/DeleteStorageBackend.php | 68 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 src/Keboola/Console/Command/DeleteStorageBackend.php diff --git a/cli.php b/cli.php index ddc6d18..6382762 100644 --- a/cli.php +++ b/cli.php @@ -6,6 +6,7 @@ use Keboola\Console\Command\AddFeature; use Keboola\Console\Command\AllStacksIterator; +use Keboola\Console\Command\DeleteStorageBackend; use Keboola\Console\Command\DeleteOrganizationOrphanedWorkspaces; use Keboola\Console\Command\DeleteOrganizationOwnerlessWorkspaces; use Keboola\Console\Command\DeleteOrphanedWorkspaces; @@ -57,4 +58,5 @@ $application->add(new UpdateDataRetention()); $application->add(new OrganizationResetWorkspacePasswords()); $application->add(new OrganizationsAddFeature()); +$application->add(new DeleteStorageBackend()); $application->run(); diff --git a/src/Keboola/Console/Command/DeleteStorageBackend.php b/src/Keboola/Console/Command/DeleteStorageBackend.php new file mode 100644 index 0000000..1a44c59 --- /dev/null +++ b/src/Keboola/Console/Command/DeleteStorageBackend.php @@ -0,0 +1,68 @@ +setName('manage:delete-backend') + ->setDescription('Set keboola.touch attribute to all tables. This will invalidate async export caches.') + ->addArgument('token', InputArgument::REQUIRED, 'storage api token') + ->addArgument('ids', InputArgument::REQUIRED, 'list of IDs separated') + ->addArgument('url', InputArgument::REQUIRED, 'Stack URL') + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Will actually do the work, otherwise it\'s dry run') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $apiToken = $input->getArgument('token'); + $apiUrl = $input->getArgument('url'); + $ids = $input->getArgument('ids'); + $force = (bool) $input->getOption('force'); + $output->writeln('DANGER: Using force mode! Backend will be removed.'); + + $client = $this->createClient($apiUrl, $apiToken); + + $allBackends = $client->listStorageBackend(); + $allBackendsAssociative = []; + foreach ($allBackends as $backend) { + $allBackendsAssociative[$backend['id']] = $backend; + } + foreach (explode(',', $ids) as $id) { + // get backend detail and print it + + $output->write(sprintf( + 'Removing backend "%s" (%s) by "%s" - ', + $id, + $allBackendsAssociative[$id]['host'], + $allBackendsAssociative[$id]['owner'] + )); + if($force){ + $output->writeln('really'); + $client->removeStorageBackend($id); + } else { + $output->writeln('just kidding - dry mode'); + } + + } + } + + + private function createClient(string $host, string $token): Client + { + return new Client([ + 'url' => $host, + 'token' => $token, + ]); + } +} From ef9136eb5ee3f4e66a0bc14869ed84c79778e2da Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Tue, 29 Jul 2025 15:37:29 +0200 Subject: [PATCH 2/5] skipping --- .../Console/Command/DeleteStorageBackend.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Keboola/Console/Command/DeleteStorageBackend.php b/src/Keboola/Console/Command/DeleteStorageBackend.php index 1a44c59..ced074b 100644 --- a/src/Keboola/Console/Command/DeleteStorageBackend.php +++ b/src/Keboola/Console/Command/DeleteStorageBackend.php @@ -19,8 +19,7 @@ protected function configure() ->addArgument('token', InputArgument::REQUIRED, 'storage api token') ->addArgument('ids', InputArgument::REQUIRED, 'list of IDs separated') ->addArgument('url', InputArgument::REQUIRED, 'Stack URL') - ->addOption('force', 'f', InputOption::VALUE_NONE, 'Will actually do the work, otherwise it\'s dry run') - ; + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Will actually do the work, otherwise it\'s dry run'); } protected function execute(InputInterface $input, OutputInterface $output) @@ -39,25 +38,25 @@ protected function execute(InputInterface $input, OutputInterface $output) $allBackendsAssociative[$backend['id']] = $backend; } foreach (explode(',', $ids) as $id) { - // get backend detail and print it - + if (!array_key_exists($id, $allBackendsAssociative)) { + $output->writeln(sprintf('Backend with ID "%s" does not exist, skipping...', $id)); + continue; + } $output->write(sprintf( 'Removing backend "%s" (%s) by "%s" - ', $id, $allBackendsAssociative[$id]['host'], $allBackendsAssociative[$id]['owner'] )); - if($force){ + if ($force) { $output->writeln('really'); $client->removeStorageBackend($id); } else { $output->writeln('just kidding - dry mode'); } - } } - private function createClient(string $host, string $token): Client { return new Client([ From 541991b04efb2ba80a95ab5b5cd85468a2461a06 Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Tue, 2 Jun 2026 14:28:32 +0200 Subject: [PATCH 3/5] Document manage:delete-backend and fix its command metadata Add README section for the DeleteStorageBackend command and correct the copy-pasted command description and misleading argument help texts. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 19 +++++++++++++++++++ .../Console/Command/DeleteStorageBackend.php | 6 +++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5e564a1..2a7e1a6 100644 --- a/README.md +++ b/README.md @@ -385,6 +385,25 @@ You can use it to set all projects of an organization to use a storage backend. php ./cli.php manage:set-organization-storage-backend [--force/-f] ``` +## Delete Storage Backend +Delete one or more storage backends from a stack by their IDs. Dry-run by default. + +- Run the command + ``` + php ./cli.php manage:delete-backend [--force/-f] + ``` +Arguments: +- manage-token (required) Manage API token for the stack. +- backend-ids (required) Comma-separated list of storage backend IDs to delete (e.g. `123,456,789`). +- stack-url (required) Full stack URL, e.g. https://connection.keboola.com. + +Options: +- --force / -f Actually remove the backends. Without it the command runs in dry-run mode and only lists what would be removed. + +Behavior: +- Lists all storage backends on the stack. +- For each requested ID: skips it (with a message) when no such backend exists; otherwise prints the backend host and owner and (with --force) removes it via the Manage API. + ## Reset Workspace passwords for projects in an organization This command is rather specific to BYODB snowflake backend migration. It resets all legacy (not keypair type) Snowflake workspace passwords for all projects in an organization. diff --git a/src/Keboola/Console/Command/DeleteStorageBackend.php b/src/Keboola/Console/Command/DeleteStorageBackend.php index ced074b..9f97d09 100644 --- a/src/Keboola/Console/Command/DeleteStorageBackend.php +++ b/src/Keboola/Console/Command/DeleteStorageBackend.php @@ -15,9 +15,9 @@ protected function configure() { $this ->setName('manage:delete-backend') - ->setDescription('Set keboola.touch attribute to all tables. This will invalidate async export caches.') - ->addArgument('token', InputArgument::REQUIRED, 'storage api token') - ->addArgument('ids', InputArgument::REQUIRED, 'list of IDs separated') + ->setDescription('Delete storage backends from a stack by their IDs. Dry-run by default.') + ->addArgument('token', InputArgument::REQUIRED, 'manage api token') + ->addArgument('ids', InputArgument::REQUIRED, 'list of storage backend IDs separated by a comma') ->addArgument('url', InputArgument::REQUIRED, 'Stack URL') ->addOption('force', 'f', InputOption::VALUE_NONE, 'Will actually do the work, otherwise it\'s dry run'); } From 06e06fe8c50d7fb4938387dd94eb59193a8a3ed8 Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Tue, 2 Jun 2026 14:38:45 +0200 Subject: [PATCH 4/5] Fix phpstan errors in DeleteStorageBackend Add int return type and cast mixed input arguments so the command passes phpstan level 9. Also only print the DANGER warning in force mode. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Keboola/Console/Command/DeleteStorageBackend.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Keboola/Console/Command/DeleteStorageBackend.php b/src/Keboola/Console/Command/DeleteStorageBackend.php index 9f97d09..0e7f651 100644 --- a/src/Keboola/Console/Command/DeleteStorageBackend.php +++ b/src/Keboola/Console/Command/DeleteStorageBackend.php @@ -22,13 +22,16 @@ protected function configure() ->addOption('force', 'f', InputOption::VALUE_NONE, 'Will actually do the work, otherwise it\'s dry run'); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $apiToken = $input->getArgument('token'); + assert(is_string($apiToken)); $apiUrl = $input->getArgument('url'); + assert(is_string($apiUrl)); $ids = $input->getArgument('ids'); + assert(is_string($ids)); $force = (bool) $input->getOption('force'); - $output->writeln('DANGER: Using force mode! Backend will be removed.'); + $output->writeln($force ? 'DANGER: Using force mode! Backend will be removed.' : 'DRY RUN'); $client = $this->createClient($apiUrl, $apiToken); @@ -50,11 +53,13 @@ protected function execute(InputInterface $input, OutputInterface $output) )); if ($force) { $output->writeln('really'); - $client->removeStorageBackend($id); + $client->removeStorageBackend((int) $id); } else { $output->writeln('just kidding - dry mode'); } } + + return 0; } private function createClient(string $host, string $token): Client From aeb1aafb3ecfb09ec3c5270d474251fb0ec94f2c Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Wed, 3 Jun 2026 11:18:38 +0200 Subject: [PATCH 5/5] Address Copilot review on DeleteStorageBackend - Declare configure(): void to match the rest of the codebase. - Trim and filter backend IDs with is_numeric before processing so that whitespace and empty entries (e.g. "123, 456,") no longer skip valid backends. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Keboola/Console/Command/DeleteStorageBackend.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Keboola/Console/Command/DeleteStorageBackend.php b/src/Keboola/Console/Command/DeleteStorageBackend.php index 0e7f651..b87a451 100644 --- a/src/Keboola/Console/Command/DeleteStorageBackend.php +++ b/src/Keboola/Console/Command/DeleteStorageBackend.php @@ -11,7 +11,7 @@ class DeleteStorageBackend extends Command { - protected function configure() + protected function configure(): void { $this ->setName('manage:delete-backend') @@ -40,7 +40,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int foreach ($allBackends as $backend) { $allBackendsAssociative[$backend['id']] = $backend; } - foreach (explode(',', $ids) as $id) { + $backendIds = array_filter(array_map('trim', explode(',', $ids)), 'is_numeric'); + foreach ($backendIds as $id) { if (!array_key_exists($id, $allBackendsAssociative)) { $output->writeln(sprintf('Backend with ID "%s" does not exist, skipping...', $id)); continue;