From 9878ba8ba308a7ef4603c9ff4889b8fb23c421c1 Mon Sep 17 00:00:00 2001 From: zajca Date: Thu, 3 Apr 2025 16:05:13 +0200 Subject: [PATCH 1/9] add cmd 'manage:mass-delete-project-workspaces' --- cli.php | 2 + composer.json | 6 +- composer.lock | 382 ++++++++++++------ .../Command/MassDeleteProjectWorkspaces.php | 185 +++++++++ 4 files changed, 441 insertions(+), 134 deletions(-) create mode 100644 src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php diff --git a/cli.php b/cli.php index 97b8f6f..38b8d0d 100644 --- a/cli.php +++ b/cli.php @@ -12,6 +12,7 @@ use Keboola\Console\Command\DescribeOrganizationWorkspaces; use Keboola\Console\Command\LineageEventsExport; use Keboola\Console\Command\MassDedup; +use Keboola\Console\Command\MassDeleteProjectWorkspaces; use Keboola\Console\Command\MassProjectEnableDynamicBackends; use Keboola\Console\Command\MassProjectExtendExpiration; use Keboola\Console\Command\MassProjectQueueMigration; @@ -59,4 +60,5 @@ $application->add(new RemoveUserFromOrganizationProjects()); $application->add(new ReactivateSchedules()); $application->add(new DescribeOrganizationWorkspaces()); +$application->add(new MassDeleteProjectWorkspaces()); $application->run(); diff --git a/composer.json b/composer.json index 9b5d552..033743a 100644 --- a/composer.json +++ b/composer.json @@ -1,12 +1,12 @@ { "name": "keboola/cli-utils", "require": { - "php": "^7.4", + "php": "^7.4|^8", "symfony/console": "^3.1", - "keboola/sandboxes-api-php-client": "^6.25", + "keboola/sandboxes-api-php-client": "^6.31", "keboola/storage-api-client": "^14", "keboola/kbc-manage-api-php-client": "^7", - "symfony/event-dispatcher": "4.1.*", + "symfony/event-dispatcher": "6.4.*", "keboola/job-queue-api-php-client": "^2.2" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 3a0c1e0..ed4037f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "87d565ff4970d63e9b93a535ab589930", + "content-hash": "facbc69fd51f1ff08754739e17053382", "packages": [ { "name": "aws/aws-crt-php", @@ -1030,35 +1030,33 @@ }, { "name": "keboola/sandboxes-api-php-client", - "version": "6.25.0", + "version": "6.31.0", "source": { "type": "git", "url": "https://github.com/keboola/sandboxes-api-php-client.git", - "reference": "bf8f0a4e26f019314c3fbdcc63900a23d4ec66e8" + "reference": "739084bc3fb50a780f33c1981af7bc72f43f0215" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/keboola/sandboxes-api-php-client/zipball/bf8f0a4e26f019314c3fbdcc63900a23d4ec66e8", - "reference": "bf8f0a4e26f019314c3fbdcc63900a23d4ec66e8", + "url": "https://api.github.com/repos/keboola/sandboxes-api-php-client/zipball/739084bc3fb50a780f33c1981af7bc72f43f0215", + "reference": "739084bc3fb50a780f33c1981af7bc72f43f0215", "shasum": "" }, "require": { "ext-json": "*", "guzzlehttp/guzzle": "~6.0|~7.0", - "php": ">=7.4", - "psr/log": "^1.1", - "symfony/validator": "^4.2 !=4.4.33|^5.4|^6.0" + "php": "^8.2", + "psr/log": "^1.1|^2.0|^3.0", + "symfony/validator": "^6.0|^7.0" }, "require-dev": { - "keboola/coding-standard": "^13.0.0", - "keboola/kbc-manage-api-php-client": "^7.0", - "keboola/storage-api-client": "^10.6.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan": "^1.5.0", - "phpstan/phpstan-phpunit": "^1.0.0", - "phpunit/phpunit": "^9.0", - "squizlabs/php_codesniffer": "^3.0", - "vlucas/phpdotenv": "^4.1" + "keboola/coding-standard": "^15.0", + "keboola/kbc-manage-api-php-client": "^7.1", + "keboola/storage-api-client": "^14.15", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.6", + "vlucas/phpdotenv": "^5.6" }, "type": "library", "autoload": { @@ -1082,9 +1080,9 @@ "sandboxes" ], "support": { - "source": "https://github.com/keboola/sandboxes-api-php-client/tree/6.25.0" + "source": "https://github.com/keboola/sandboxes-api-php-client/tree/6.31.0" }, - "time": "2023-08-07T14:39:54+00:00" + "time": "2025-02-19T07:20:50+00:00" }, { "name": "keboola/storage-api-client", @@ -1458,6 +1456,56 @@ }, "time": "2016-08-06T20:24:11+00:00" }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, { "name": "psr/http-client", "version": "1.0.2", @@ -2263,41 +2311,41 @@ }, { "name": "symfony/event-dispatcher", - "version": "v4.1.12", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "51be1b61dfe04d64a260223f2b81475fa8066b97" + "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/51be1b61dfe04d64a260223f2b81475fa8066b97", - "reference": "51be1b61dfe04d64a260223f2b81475fa8066b97", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", + "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<3.4" + "symfony/dependency-injection": "<5.4", + "symfony/service-contracts": "<2.5" }, - "require-dev": { - "psr/log": "~1.0", - "symfony/config": "~3.4|~4.0", - "symfony/dependency-injection": "~3.4|~4.0", - "symfony/expression-language": "~3.4|~4.0", - "symfony/stopwatch": "~3.4|~4.0" + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^5.4|^6.0|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\EventDispatcher\\": "" @@ -2320,12 +2368,102 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony EventDispatcher Component", + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:18:03+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v4.1.12" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" }, - "time": "2019-01-16T18:35:49+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/filesystem", @@ -2557,17 +2695,17 @@ "time": "2022-11-03T14:55:06+00:00" }, { - "name": "symfony/polyfill-php73", + "name": "symfony/polyfill-php80", "version": "v1.27.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/9e8ecb5f92152187c4799efd3c96b78ccab18ff9", - "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", "shasum": "" }, "require": { @@ -2588,7 +2726,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" + "Symfony\\Polyfill\\Php80\\": "" }, "classmap": [ "Resources/stubs" @@ -2599,6 +2737,10 @@ "MIT" ], "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -2608,7 +2750,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -2617,7 +2759,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" }, "funding": [ { @@ -2636,17 +2778,17 @@ "time": "2022-11-03T14:55:06+00:00" }, { - "name": "symfony/polyfill-php80", + "name": "symfony/polyfill-php81", "version": "v1.27.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/707403074c8ea6e2edaf8794b0157a0bfa52157a", + "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a", "shasum": "" }, "require": { @@ -2667,7 +2809,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" + "Symfony\\Polyfill\\Php81\\": "" }, "classmap": [ "Resources/stubs" @@ -2678,10 +2820,6 @@ "MIT" ], "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -2691,7 +2829,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -2700,7 +2838,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.27.0" }, "funding": [ { @@ -2719,30 +2857,27 @@ "time": "2022-11-03T14:55:06+00:00" }, { - "name": "symfony/polyfill-php81", - "version": "v1.27.0", + "name": "symfony/polyfill-php83", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a" + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/707403074c8ea6e2edaf8794b0157a0bfa52157a", - "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -2750,7 +2885,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php81\\": "" + "Symfony\\Polyfill\\Php83\\": "" }, "classmap": [ "Resources/stubs" @@ -2770,7 +2905,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -2779,7 +2914,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" }, "funding": [ { @@ -2795,7 +2930,7 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/process", @@ -2939,71 +3074,55 @@ }, { "name": "symfony/validator", - "version": "v5.4.26", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "77533f12c6dd5c766f1e1689de4ef4d1eac4af71" + "reference": "9314555aceb8d8ce8abda81e1e47e439258d9309" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/77533f12c6dd5c766f1e1689de4ef4d1eac4af71", - "reference": "77533f12c6dd5c766f1e1689de4ef4d1eac4af71", + "url": "https://api.github.com/repos/symfony/validator/zipball/9314555aceb8d8ce8abda81e1e47e439258d9309", + "reference": "9314555aceb8d8ce8abda81e1e47e439258d9309", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "~1.0", - "symfony/polyfill-php80": "^1.16", - "symfony/polyfill-php81": "^1.22", - "symfony/translation-contracts": "^1.1|^2|^3" + "symfony/polyfill-php83": "^1.27", + "symfony/translation-contracts": "^2.5|^3" }, "conflict": { "doctrine/annotations": "<1.13", - "doctrine/cache": "<1.11", "doctrine/lexer": "<1.1", - "symfony/dependency-injection": "<4.4", - "symfony/expression-language": "<5.1", - "symfony/http-kernel": "<4.4", - "symfony/intl": "<4.4", - "symfony/property-info": "<5.3", - "symfony/translation": "<4.4", - "symfony/yaml": "<4.4" + "symfony/dependency-injection": "<5.4", + "symfony/expression-language": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/intl": "<5.4", + "symfony/property-info": "<5.4", + "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3", + "symfony/yaml": "<5.4" }, "require-dev": { "doctrine/annotations": "^1.13|^2", - "doctrine/cache": "^1.11|^2.0", "egulias/email-validator": "^2.1.10|^3|^4", - "symfony/cache": "^4.4|^5.0|^6.0", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/console": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^5.1|^6.0", - "symfony/finder": "^4.4|^5.0|^6.0", - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^4.4|^5.0|^6.0", - "symfony/intl": "^4.4|^5.0|^6.0", - "symfony/mime": "^4.4|^5.0|^6.0", - "symfony/property-access": "^4.4|^5.0|^6.0", - "symfony/property-info": "^5.3|^6.0", - "symfony/translation": "^4.4|^5.0|^6.0", - "symfony/yaml": "^4.4|^5.0|^6.0" - }, - "suggest": { - "egulias/email-validator": "Strict (RFC compliant) email validation", - "psr/cache-implementation": "For using the mapping cache.", - "symfony/config": "", - "symfony/expression-language": "For using the Expression validator and the ExpressionLanguageSyntax constraints", - "symfony/http-foundation": "", - "symfony/intl": "", - "symfony/property-access": "For accessing properties within comparison constraints", - "symfony/property-info": "To automatically add NotNull and Type constraints", - "symfony/translation": "For translating validation errors.", - "symfony/yaml": "" + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -3011,7 +3130,8 @@ "Symfony\\Component\\Validator\\": "" }, "exclude-from-classmap": [ - "/Tests/" + "/Tests/", + "/Resources/bin/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -3031,7 +3151,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v5.4.26" + "source": "https://github.com/symfony/validator/tree/v6.4.20" }, "funding": [ { @@ -3047,7 +3167,7 @@ "type": "tidelift" } ], - "time": "2023-07-26T15:05:40+00:00" + "time": "2025-03-14T14:22:58+00:00" } ], "packages-dev": [ @@ -3137,12 +3257,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^7.4" + "php": "^7.4|^8" }, - "platform-dev": [], - "plugin-api-version": "2.3.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php b/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php new file mode 100644 index 0000000..50a8ec7 --- /dev/null +++ b/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php @@ -0,0 +1,185 @@ +setName('manage:mass-delete-project-workspaces') + ->setDescription('Mass project enable dynamic backends') + ->addArgument(self::ARGUMENT_STACK_SUFFIX, InputArgument::REQUIRED, 'stack suffix "keboola.com, eu-central-1.keboola.com"') + ->addArgument(self::ARGUMENT_SOURCE_FILE, InputArgument::REQUIRED, 'Source csv with "prjId,workspaceSchema" columns') + ->addOption(self::OPTION_FORCE, 'f', InputOption::VALUE_NONE, 'Write changes'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $connectionUrl = 'https://connection.' . $input->getArgument(self::ARGUMENT_STACK_SUFFIX); + $sandboxesUrl = 'https://sandboxes.' . $input->getArgument(self::ARGUMENT_STACK_SUFFIX); + $sourceFile = $input->getArgument(self::ARGUMENT_SOURCE_FILE); + $output->writeln(sprintf('Fetching projects from "%s"', $sourceFile)); + $force = $input->getOption(self::OPTION_FORCE); + + // map by project id + /** + * @var array{ + * string, + * string[], + * } $map + */ + $map = []; + $csv = new CsvFile($sourceFile); + foreach ($csv as $i => $line) { + if ($i === 0) { + // skip header + continue; + } + if (array_key_exists($line[0], $map)) { + $map[$line[0]][] = $line[1]; + } else { + $map[$line[0]] = [$line[1]]; + } + } + + // testing override +// $map = [ +// '232' => [ +// 'WORKSPACE_832798053', +// 'WORKSPACE_965913339', +// ], +// ]; + + foreach ($map as $projectId => $workspaces) { + $helper = $this->getHelper('question'); + $question = new Question(sprintf( + 'Paster storage token for project "%s" to continue.' . PHP_EOL, + $projectId, + )); + $storageToken = $helper->ask($input, $output, $question); + + $storageClient = new Client([ + 'token' => $storageToken, + 'url' => $connectionUrl, + ]); + $workspacesClient = new Workspaces($storageClient); + $componentsClient = new Components($storageClient); + $sandboxesClient = new SandboxesClient( + $sandboxesUrl, + $storageToken + ); + + /** @var Sandbox $sandbox */ + foreach ($sandboxesClient->list() as $sandbox) { + if ($sandbox->getWorkspaceDetails() === [] || !array_key_exists('connection', $sandbox->getWorkspaceDetails())) { + continue; // skip sandboxes + } + foreach ($workspaces as $schema) { + $output->writeln(sprintf('Checking sandbox "%s" with schema "%s"', $sandbox->getWorkspaceDetails()['connection']['schema'], $schema)); + if ($schema === $sandbox->getWorkspaceDetails()['connection']['schema']) { + $output->writeln(sprintf( + 'Sandbox "%s" with schema "%s" found.', + $sandbox->getId(), + $schema, + )); + // remove found schema from map + unset($map[$projectId][array_search($schema, $map[$projectId])]); + + $output->writeln('Looking for configuration.'); + $configuration = null; + try { + $configuration = $componentsClient->getConfiguration('keboola.sandboxes', $sandbox->getConfigurationId()); + $output->writeln(sprintf( + 'Configuration "%s" found.', + $configuration['id'], + )); + } catch (StorageApiClientException $e) { + $output->writeln(sprintf( + 'Configuration "keboola.sandboxes"->"%s" not found.', + $sandbox->getConfigurationId(), + )); + } + + $output->writeln('Looking for storage workspace.'); + $storageWorkspace = null; + try { + $storageWorkspace = $workspacesClient->getWorkspace($sandbox->getPhysicalId()); + $output->writeln(sprintf( + 'Storage workspace "%s" found.', + $storageWorkspace['id'], + )); + } catch (StorageApiClientException $e) { + $output->writeln(sprintf( + 'Workspace "%s" not found.', + $sandbox->getPhysicalId(), + )); + } + // workspace is sandbox and we can delete configuration,sandbox and workspace + if ($force) { + $output->writeln(sprintf('Deleting sandbox "%s" with schema "%s"', $sandbox->getId(), $schema)); + $sandboxesClient->delete($sandbox->getId()); + if ($configuration !== null) { + $output->writeln(sprintf('Deleting configuration "%s"', $configuration['id'])); + $componentsClient->deleteConfiguration('keboola.sandboxes', $configuration['id']); + } + if ($storageWorkspace !== null) { + $output->writeln(sprintf('Deleting storage workspace "%s"', $storageWorkspace['id'])); + $workspacesClient->deleteWorkspace($storageWorkspace['id']); + } + } else { + $output->writeln('[DRY-RUN] Resources would be deleted'); + } + } + } + } + + foreach ($workspacesClient->listWorkspaces() as $workspace) { + foreach ($map[$projectId] as $workspaceOnlyInStorage) { + if ($workspace['connection']['schema'] === $workspaceOnlyInStorage) { + $output->writeln(sprintf( + 'Workspace "%s" with schema "%s" found.', + $workspace['id'], + $workspaceOnlyInStorage, + )); + // remove found schema from map + unset($map[$projectId][array_search($workspaceOnlyInStorage, $map[$projectId])]); + if ($force) { + $output->writeln(sprintf('Deleting workspace "%s" with schema "%s"', $workspace['id'], $workspaceOnlyInStorage)); + $workspacesClient->deleteWorkspace($workspace['id']); + } else { + $output->writeln('[DRY-RUN] Resources would be deleted'); + } + } + } + } + + if (count($map[$projectId]) !== 0) { + $output->writeln([ + sprintf('Following schemas was not found and needs to be deleted manually:'), + ...$map[$projectId], + ]); + } + } + } +} From dee87e72a17942a7e0805749c0393103c59f33c8 Mon Sep 17 00:00:00 2001 From: zajca Date: Thu, 3 Apr 2025 16:15:22 +0200 Subject: [PATCH 2/9] update php --- Dockerfile | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index f986772..fc989ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM php:7.4 +FROM php:8.2-fpm WORKDIR /code diff --git a/composer.json b/composer.json index 033743a..c701292 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "keboola/cli-utils", "require": { - "php": "^7.4|^8", + "php": "^8.2", "symfony/console": "^3.1", "keboola/sandboxes-api-php-client": "^6.31", "keboola/storage-api-client": "^14", From 5d681f715dc2e39ee623e76d8703fead2f3982fa Mon Sep 17 00:00:00 2001 From: zajca Date: Thu, 3 Apr 2025 16:17:27 +0200 Subject: [PATCH 3/9] update codesiffer --- composer.json | 2 +- composer.lock | 93 ++++++++++++++++++++++++++------------------------- 2 files changed, 48 insertions(+), 47 deletions(-) diff --git a/composer.json b/composer.json index c701292..be7f3a8 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ "keboola/job-queue-api-php-client": "^2.2" }, "require-dev": { - "squizlabs/php_codesniffer": "2.*" + "squizlabs/php_codesniffer": "3.*" }, "authors": [ { diff --git a/composer.lock b/composer.lock index ed4037f..5bc83b7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "facbc69fd51f1ff08754739e17053382", + "content-hash": "1cb691a3a5beacf61fd6bf492bf0438b", "packages": [ { "name": "aws/aws-crt-php", @@ -3173,64 +3173,37 @@ "packages-dev": [ { "name": "squizlabs/php_codesniffer", - "version": "2.9.2", + "version": "3.12.0", "source": { "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "2acf168de78487db620ab4bc524135a13cfe6745" + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "2d1b63db139c3c6ea0c927698e5160f8b3b8d630" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/2acf168de78487db620ab4bc524135a13cfe6745", - "reference": "2acf168de78487db620ab4bc524135a13cfe6745", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/2d1b63db139c3c6ea0c927698e5160f8b3b8d630", + "reference": "2d1b63db139c3c6ea0c927698e5160f8b3b8d630", "shasum": "" }, "require": { "ext-simplexml": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": ">=5.1.2" + "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, "bin": [ - "scripts/phpcs", - "scripts/phpcbf" + "bin/phpcbf", + "bin/phpcs" ], "type": "library", "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-master": "3.x-dev" } }, - "autoload": { - "classmap": [ - "CodeSniffer.php", - "CodeSniffer/CLI.php", - "CodeSniffer/Exception.php", - "CodeSniffer/File.php", - "CodeSniffer/Fixer.php", - "CodeSniffer/Report.php", - "CodeSniffer/Reporting.php", - "CodeSniffer/Sniff.php", - "CodeSniffer/Tokens.php", - "CodeSniffer/Reports/", - "CodeSniffer/Tokenizers/", - "CodeSniffer/DocGenerators/", - "CodeSniffer/Standards/AbstractPatternSniff.php", - "CodeSniffer/Standards/AbstractScopeSniff.php", - "CodeSniffer/Standards/AbstractVariableSniff.php", - "CodeSniffer/Standards/IncorrectPatternException.php", - "CodeSniffer/Standards/Generic/Sniffs/", - "CodeSniffer/Standards/MySource/Sniffs/", - "CodeSniffer/Standards/PEAR/Sniffs/", - "CodeSniffer/Standards/PSR1/Sniffs/", - "CodeSniffer/Standards/PSR2/Sniffs/", - "CodeSniffer/Standards/Squiz/Sniffs/", - "CodeSniffer/Standards/Zend/Sniffs/" - ] - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" @@ -3238,21 +3211,49 @@ "authors": [ { "name": "Greg Sherwood", - "role": "lead" + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "http://www.squizlabs.com/php-codesniffer", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", "keywords": [ "phpcs", - "standards" + "standards", + "static analysis" ], "support": { - "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", - "source": "https://github.com/squizlabs/PHP_CodeSniffer", - "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" }, - "time": "2018-11-07T22:31:41+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-03-18T05:04:51+00:00" } ], "aliases": [], @@ -3261,7 +3262,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^7.4|^8" + "php": "^8.2" }, "platform-dev": {}, "plugin-api-version": "2.6.0" From a9f806801599cd663474aec3bf1e67a6beb9027b Mon Sep 17 00:00:00 2001 From: zajca Date: Thu, 3 Apr 2025 16:19:37 +0200 Subject: [PATCH 4/9] wording fix --- src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php b/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php index 50a8ec7..6bf2424 100644 --- a/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php +++ b/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php @@ -28,7 +28,7 @@ protected function configure() { $this ->setName('manage:mass-delete-project-workspaces') - ->setDescription('Mass project enable dynamic backends') + ->setDescription('Delete all project workspaces based on given list in file. [Works only for SNFLK now].') ->addArgument(self::ARGUMENT_STACK_SUFFIX, InputArgument::REQUIRED, 'stack suffix "keboola.com, eu-central-1.keboola.com"') ->addArgument(self::ARGUMENT_SOURCE_FILE, InputArgument::REQUIRED, 'Source csv with "prjId,workspaceSchema" columns') ->addOption(self::OPTION_FORCE, 'f', InputOption::VALUE_NONE, 'Write changes'); @@ -176,7 +176,7 @@ protected function execute(InputInterface $input, OutputInterface $output) if (count($map[$projectId]) !== 0) { $output->writeln([ - sprintf('Following schemas was not found and needs to be deleted manually:'), + sprintf('Following schemas were not found and needs to be deleted manually:'), ...$map[$projectId], ]); } From 1481b650784682db41f4bc5054d22f7ba4181d0c Mon Sep 17 00:00:00 2001 From: zajca Date: Thu, 3 Apr 2025 20:54:04 +0200 Subject: [PATCH 5/9] use job queue to delete sandboxes --- .../Command/MassDeleteProjectWorkspaces.php | 166 +++++++++--------- 1 file changed, 87 insertions(+), 79 deletions(-) diff --git a/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php b/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php index 6bf2424..8c4c518 100644 --- a/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php +++ b/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php @@ -5,11 +5,13 @@ namespace Keboola\Console\Command; use Keboola\Csv\CsvFile; +use Keboola\JobQueueClient\JobData; use Keboola\Sandboxes\Api\Client as SandboxesClient; +use Keboola\JobQueueClient\Client as QueueClient; +use Keboola\Sandboxes\Api\ListOptions; use Keboola\Sandboxes\Api\Sandbox; use Keboola\StorageApi\Client; -use Keboola\StorageApi\ClientException as StorageApiClientException; -use Keboola\StorageApi\Components; +use Keboola\StorageApi\DevBranches; use Keboola\StorageApi\Workspaces; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -38,6 +40,7 @@ protected function execute(InputInterface $input, OutputInterface $output) { $connectionUrl = 'https://connection.' . $input->getArgument(self::ARGUMENT_STACK_SUFFIX); $sandboxesUrl = 'https://sandboxes.' . $input->getArgument(self::ARGUMENT_STACK_SUFFIX); + $jobsUrl = 'https://queue.' . $input->getArgument(self::ARGUMENT_STACK_SUFFIX); $sourceFile = $input->getArgument(self::ARGUMENT_SOURCE_FILE); $output->writeln(sprintf('Fetching projects from "%s"', $sourceFile)); $force = $input->getOption(self::OPTION_FORCE); @@ -71,10 +74,10 @@ protected function execute(InputInterface $input, OutputInterface $output) // ], // ]; - foreach ($map as $projectId => $workspaces) { + foreach ($map as $projectId => $workspacesSchemasToDelete) { $helper = $this->getHelper('question'); $question = new Question(sprintf( - 'Paster storage token for project "%s" to continue.' . PHP_EOL, + 'Paste storage token for project "%s" to continue.' . PHP_EOL, $projectId, )); $storageToken = $helper->ask($input, $output, $question); @@ -83,101 +86,106 @@ protected function execute(InputInterface $input, OutputInterface $output) 'token' => $storageToken, 'url' => $connectionUrl, ]); - $workspacesClient = new Workspaces($storageClient); - $componentsClient = new Components($storageClient); $sandboxesClient = new SandboxesClient( $sandboxesUrl, $storageToken ); + $jobsClient = new QueueClient( + $jobsUrl, + $storageToken + ); - /** @var Sandbox $sandbox */ - foreach ($sandboxesClient->list() as $sandbox) { - if ($sandbox->getWorkspaceDetails() === [] || !array_key_exists('connection', $sandbox->getWorkspaceDetails())) { - continue; // skip sandboxes + $branchesClient = new DevBranches($storageClient); + + $jobs = []; + foreach ($branchesClient->listBranches() as $branch) { + $output->writeln(sprintf('Checking branch "%s" for sandboxes.', $branch['id'])); + $branchId = (string) $branch['id']; + if ($branch['isDefault']) { + $branchId = null; } - foreach ($workspaces as $schema) { - $output->writeln(sprintf('Checking sandbox "%s" with schema "%s"', $sandbox->getWorkspaceDetails()['connection']['schema'], $schema)); - if ($schema === $sandbox->getWorkspaceDetails()['connection']['schema']) { + /** @var Sandbox $sandbox */ + foreach ($sandboxesClient->list((new ListOptions())->setBranchId($branchId)) as $sandbox) { + $schema = $sandbox->getWorkspaceDetails()['connection']['schema'] ?? null; + if (!in_array($schema, $workspacesSchemasToDelete, true)) { + continue; + } + $output->writeln(sprintf( + 'Sandbox "%s" with schema "%s" found.', + $sandbox->getId(), + $schema, + )); + + // remove found schema from map + unset($map[$projectId][array_search($schema, $map[$projectId], true)]); + + if ($force) { + $jobs[] = $job = $jobsClient->createJob(new JobData( + 'keboola.sandboxes', + null, + [ + 'parameters' => [ + 'task' => 'delete', + 'id' => $sandbox->getId(), + ], + ], + + )); $output->writeln(sprintf( - 'Sandbox "%s" with schema "%s" found.', - $sandbox->getId(), - $schema, + 'Created delete job "%s" for project "%s"', + $job['id'], + $projectId + )); + } else { + $output->writeln(sprintf( + '[DRY-RUN] Created delete job "%s" for project "%s"', + '', + $projectId )); - // remove found schema from map - unset($map[$projectId][array_search($schema, $map[$projectId])]); - - $output->writeln('Looking for configuration.'); - $configuration = null; - try { - $configuration = $componentsClient->getConfiguration('keboola.sandboxes', $sandbox->getConfigurationId()); - $output->writeln(sprintf( - 'Configuration "%s" found.', - $configuration['id'], - )); - } catch (StorageApiClientException $e) { - $output->writeln(sprintf( - 'Configuration "keboola.sandboxes"->"%s" not found.', - $sandbox->getConfigurationId(), - )); - } - - $output->writeln('Looking for storage workspace.'); - $storageWorkspace = null; - try { - $storageWorkspace = $workspacesClient->getWorkspace($sandbox->getPhysicalId()); - $output->writeln(sprintf( - 'Storage workspace "%s" found.', - $storageWorkspace['id'], - )); - } catch (StorageApiClientException $e) { - $output->writeln(sprintf( - 'Workspace "%s" not found.', - $sandbox->getPhysicalId(), - )); - } - // workspace is sandbox and we can delete configuration,sandbox and workspace - if ($force) { - $output->writeln(sprintf('Deleting sandbox "%s" with schema "%s"', $sandbox->getId(), $schema)); - $sandboxesClient->delete($sandbox->getId()); - if ($configuration !== null) { - $output->writeln(sprintf('Deleting configuration "%s"', $configuration['id'])); - $componentsClient->deleteConfiguration('keboola.sandboxes', $configuration['id']); - } - if ($storageWorkspace !== null) { - $output->writeln(sprintf('Deleting storage workspace "%s"', $storageWorkspace['id'])); - $workspacesClient->deleteWorkspace($storageWorkspace['id']); - } - } else { - $output->writeln('[DRY-RUN] Resources would be deleted'); - } } } } - foreach ($workspacesClient->listWorkspaces() as $workspace) { - foreach ($map[$projectId] as $workspaceOnlyInStorage) { - if ($workspace['connection']['schema'] === $workspaceOnlyInStorage) { + $output->writeln('Waiting for delete jobs to finish.'); + while (count($jobs) > 0) { + foreach ($jobs as $i => $job) { + $jobRes = $jobsClient->getJob((string) $job['id']); + if ($jobRes['isFinished'] === true) { $output->writeln(sprintf( - 'Workspace "%s" with schema "%s" found.', - $workspace['id'], - $workspaceOnlyInStorage, + 'Delete job "%s" finished with status "%s"', + $job['id'], + $jobRes['status'] )); - // remove found schema from map - unset($map[$projectId][array_search($workspaceOnlyInStorage, $map[$projectId])]); - if ($force) { - $output->writeln(sprintf('Deleting workspace "%s" with schema "%s"', $workspace['id'], $workspaceOnlyInStorage)); - $workspacesClient->deleteWorkspace($workspace['id']); - } else { - $output->writeln('[DRY-RUN] Resources would be deleted'); - } + unset($jobs[$i]); + } + } + sleep(2); + } + + foreach ($branchesClient->listBranches() as $branch) { + $output->writeln(sprintf('Checking branch "%s" for storage workspaces.', $branch['id'])); + $workspacesClient = new Workspaces( + $storageClient->getBranchAwareClient($branch['id']) + ); + foreach ($workspacesClient->listWorkspaces() as $workspace) { + if (!in_array($workspace['connection']['schema'], $map[$projectId], true)) { + continue; + } + // remove found schema from map + unset($map[$projectId][array_search($workspace['connection']['schema'], $map[$projectId], true)]); + if ($force) { + $output->writeln(sprintf('Deleting workspace "%s" with schema "%s"', $workspace['id'], $workspace['connection']['schema'])); + $workspacesClient->deleteWorkspace($workspace['id']); + } else { + $output->writeln(sprintf('[DRY-RUN] Deleting workspace "%s" with schema "%s"', $workspace['id'], $workspace['connection']['schema'])); } } } if (count($map[$projectId]) !== 0) { $output->writeln([ - sprintf('Following schemas were not found and needs to be deleted manually:'), - ...$map[$projectId], + 'Following schemas were not found (are deleted or needs to be deleted manually): %s', + implode(', ', $map[$projectId]), ]); } } From f4b150b0d0dfb3d2b592a28c83a7308f28574265 Mon Sep 17 00:00:00 2001 From: zajca Date: Thu, 3 Apr 2025 20:59:39 +0200 Subject: [PATCH 6/9] add csv validation --- .../Command/MassDeleteProjectWorkspaces.php | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php b/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php index 8c4c518..4e973ed 100644 --- a/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php +++ b/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php @@ -4,6 +4,7 @@ namespace Keboola\Console\Command; +use InvalidArgumentException; use Keboola\Csv\CsvFile; use Keboola\JobQueueClient\JobData; use Keboola\Sandboxes\Api\Client as SandboxesClient; @@ -32,7 +33,7 @@ protected function configure() ->setName('manage:mass-delete-project-workspaces') ->setDescription('Delete all project workspaces based on given list in file. [Works only for SNFLK now].') ->addArgument(self::ARGUMENT_STACK_SUFFIX, InputArgument::REQUIRED, 'stack suffix "keboola.com, eu-central-1.keboola.com"') - ->addArgument(self::ARGUMENT_SOURCE_FILE, InputArgument::REQUIRED, 'Source csv with "prjId,workspaceSchema" columns') + ->addArgument(self::ARGUMENT_SOURCE_FILE, InputArgument::REQUIRED, 'Source csv with "project id,workspace schema" columns and no header.') ->addOption(self::OPTION_FORCE, 'f', InputOption::VALUE_NONE, 'Write changes'); } @@ -54,11 +55,17 @@ protected function execute(InputInterface $input, OutputInterface $output) */ $map = []; $csv = new CsvFile($sourceFile); - foreach ($csv as $i => $line) { - if ($i === 0) { - // skip header - continue; + foreach ($csv as $line) { + if (count($line) !== 2) { + throw new InvalidArgumentException('File must contain exactly two columns.'); } + if (!is_numeric($line[0])) { + throw new InvalidArgumentException(sprintf('Project id "%s" is not numeric.', $line[0])); + } + if (!str_starts_with($line[1], 'WORKSPACE_')) { + throw new InvalidArgumentException(sprintf('Workspace "%s" does not start with "WORKSPACE_".', $line[1])); + } + if (array_key_exists($line[0], $map)) { $map[$line[0]][] = $line[1]; } else { From 8e43dbdf84cd39411347486b7a89fce314125e92 Mon Sep 17 00:00:00 2001 From: zajca Date: Thu, 3 Apr 2025 21:03:13 +0200 Subject: [PATCH 7/9] cs --- src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php b/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php index 4e973ed..64d5677 100644 --- a/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php +++ b/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php @@ -136,7 +136,6 @@ protected function execute(InputInterface $input, OutputInterface $output) 'id' => $sandbox->getId(), ], ], - )); $output->writeln(sprintf( 'Created delete job "%s" for project "%s"', From 3525a71a9f867e368b609eaff6a2620ed0c94708 Mon Sep 17 00:00:00 2001 From: zajca Date: Fri, 4 Apr 2025 08:09:44 +0200 Subject: [PATCH 8/9] use php:8.2-cli --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index fc989ce..904f3d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.2-fpm +FROM php:8.2-cli WORKDIR /code From ead554a8dcc581c021d89978f54a6407eb1c58db Mon Sep 17 00:00:00 2001 From: zajca Date: Wed, 9 Apr 2025 22:03:56 +0200 Subject: [PATCH 9/9] improve errors, async workspace drop --- .../Command/MassDeleteProjectWorkspaces.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php b/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php index 64d5677..72d57ce 100644 --- a/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php +++ b/src/Keboola/Console/Command/MassDeleteProjectWorkspaces.php @@ -127,7 +127,7 @@ protected function execute(InputInterface $input, OutputInterface $output) unset($map[$projectId][array_search($schema, $map[$projectId], true)]); if ($force) { - $jobs[] = $job = $jobsClient->createJob(new JobData( + $job = $jobsClient->createJob(new JobData( 'keboola.sandboxes', null, [ @@ -137,6 +137,10 @@ protected function execute(InputInterface $input, OutputInterface $output) ], ], )); + + $job['sandbox'] = $sandbox; + $jobs[] = $job; + $output->writeln(sprintf( 'Created delete job "%s" for project "%s"', $job['id'], @@ -158,8 +162,10 @@ protected function execute(InputInterface $input, OutputInterface $output) $jobRes = $jobsClient->getJob((string) $job['id']); if ($jobRes['isFinished'] === true) { $output->writeln(sprintf( - 'Delete job "%s" finished with status "%s"', + 'Delete job "%s" for sandbox "%s" with schema "%s" finished with status "%s"', $job['id'], + $job['sandbox']->getId(), + $job['sandbox']->getWorkspaceDetails()['connection']['schema'], $jobRes['status'] )); unset($jobs[$i]); @@ -181,7 +187,7 @@ protected function execute(InputInterface $input, OutputInterface $output) unset($map[$projectId][array_search($workspace['connection']['schema'], $map[$projectId], true)]); if ($force) { $output->writeln(sprintf('Deleting workspace "%s" with schema "%s"', $workspace['id'], $workspace['connection']['schema'])); - $workspacesClient->deleteWorkspace($workspace['id']); + $workspacesClient->deleteWorkspace($workspace['id'], [], true); } else { $output->writeln(sprintf('[DRY-RUN] Deleting workspace "%s" with schema "%s"', $workspace['id'], $workspace['connection']['schema'])); } @@ -190,7 +196,7 @@ protected function execute(InputInterface $input, OutputInterface $output) if (count($map[$projectId]) !== 0) { $output->writeln([ - 'Following schemas were not found (are deleted or needs to be deleted manually): %s', + 'Following schemas were not found (are deleted or needs to be deleted manually):', implode(', ', $map[$projectId]), ]); }