From 1fa6599e612f884c9d1573aa970ff91320b09a2b Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Fri, 19 Dec 2025 14:30:03 +0100 Subject: [PATCH 1/8] Add command to distribute feature accross org --- cli.php | 2 + .../Command/OrganizationsAddFeature.php | 79 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 src/Keboola/Console/Command/OrganizationsAddFeature.php diff --git a/cli.php b/cli.php index eecf6a7..6d8c865 100644 --- a/cli.php +++ b/cli.php @@ -16,6 +16,7 @@ use Keboola\Console\Command\MassProjectExtendExpiration; use Keboola\Console\Command\OrganizationIntoMaintenanceMode; use Keboola\Console\Command\OrganizationResetWorkspacePasswords; +use Keboola\Console\Command\OrganizationsAddFeature; use Keboola\Console\Command\OrganizationStorageBackend; use Keboola\Console\Command\QueueMassTerminateJobs; use Keboola\Console\Command\ReactivateSchedules; @@ -53,4 +54,5 @@ $application->add(new MassDeleteProjectWorkspaces()); $application->add(new UpdateDataRetention()); $application->add(new OrganizationResetWorkspacePasswords()); +$application->add(new OrganizationsAddFeature()); $application->run(); diff --git a/src/Keboola/Console/Command/OrganizationsAddFeature.php b/src/Keboola/Console/Command/OrganizationsAddFeature.php new file mode 100644 index 0000000..4e60bd2 --- /dev/null +++ b/src/Keboola/Console/Command/OrganizationsAddFeature.php @@ -0,0 +1,79 @@ +setName('manage:organizations-add-feature') + ->setDescription('Add feature to all projects in organizations.') + ->addArgument(self::ARG_TOKEN, InputArgument::REQUIRED, 'manage token') + ->addArgument(self::ARG_URL, InputArgument::REQUIRED, 'Stack URL') + ->addArgument(self::ARG_FEATURE, InputArgument::REQUIRED, 'feature') + ->addArgument(self::ARG_ORGANIZATIONS, InputArgument::REQUIRED, 'list of IDs separated by comma') + ->addOption(self::OPT_FORCE, 'f', InputOption::VALUE_NONE, 'Will actually do the work, otherwise it\'s dry run'); + } + + public function execute(InputInterface $input, OutputInterface $output): ?int + { + $args = $input->getArguments(); + $force = (bool) $input->getOption(self::OPT_FORCE); + $featureName = $args[self::ARG_FEATURE]; + $orgIDsArg = $args[self::ARG_ORGANIZATIONS]; + $client = $this->createClient($args[self::ARG_URL], $args[self::ARG_TOKEN]); + + if (!$this->checkIfFeatureExists($client, $featureName)) { + $output->writeln(sprintf('Feature %s does NOT exist', $featureName)); + return 1; + } + + $orgIds = array_filter(explode(',', $orgIDsArg), 'is_numeric'); + foreach ($orgIds as $orgId) { + $orgDetail = $client->getOrganization($orgId); + $output->writeln(sprintf('Adding feature to organization "%s" ("%s")', $orgDetail['id'], $orgDetail['name'])); + $projectIds = array_map(fn($prj) => $prj['id'], $orgDetail['projects']); + $this->addFeatureToSelectedProjects($client, $output, $featureName, $projectIds, $force); + } + + $output->writeln("\nDONE with following results:\n"); + $this->printResult($output, $force); + + return 0; + } + + private function printResult(OutputInterface $output, bool $force): void + { + $output->writeln(sprintf( + "%d projects where disabled\n" + . "%d projects have the feature already\n" + . '%d ' . ($force ? "projects updated" : "projects can be updated in force mode") . "\n", + $this->projectsDisabled, + $this->projectsWithFeature, + $this->projectsUpdated + ) + ); + } +} From d93192ab3535fb30dcbba3cbd3d28b89b773c88a Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Fri, 19 Dec 2025 14:31:52 +0100 Subject: [PATCH 2/8] docs --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 95b8f12..9ef0e88 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,18 @@ By in argument `` you can Note: the feature has to exist before calling, and it has to be type of `project` +### Add Feature on all projects in Organization +Adds a project feature to all projects in selected organizations + +``` +php cli.php manage:organizations-add-feature [-f|--force] +``` +By in argument `` you can +- select projects to add the feature to by specifying organization IDs separated by a comma (e.g. `1,2,3,4`) + - `manage:organizations-add-feature 1,2,3` + +Note: the feature has to exist before calling, and it has to be type of `project` + ### Bulk Project Remove Feature Removes a project feature from multiple projects From 3e2ccb434fe546812b1a0a38aaa385181d7ef182 Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Fri, 19 Dec 2025 14:36:08 +0100 Subject: [PATCH 3/8] fix --- src/Keboola/Console/Command/ProjectsAddFeature.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Keboola/Console/Command/ProjectsAddFeature.php b/src/Keboola/Console/Command/ProjectsAddFeature.php index 3c14df2..8d032b6 100644 --- a/src/Keboola/Console/Command/ProjectsAddFeature.php +++ b/src/Keboola/Console/Command/ProjectsAddFeature.php @@ -52,7 +52,7 @@ protected function createClient(string $host, string $token): Client */ protected function addFeatureToProject(Client $client, OutputInterface $output, array $projectInfo, string $featureName, bool $force): void { - $projectId = $projectInfo['id']; + $projectId = (string) $projectInfo['id']; assert(is_string($projectId)); $projectId = is_numeric($projectId) ? (int) $projectId : (int) $projectId; $output->writeln("Adding feature to project " . $projectId); From 8ccefd9bbfbab65a62576ef78d70ba75d031ce3b Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Fri, 19 Dec 2025 14:55:25 +0100 Subject: [PATCH 4/8] recover from failed orgs --- .../Command/OrganizationsAddFeature.php | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Keboola/Console/Command/OrganizationsAddFeature.php b/src/Keboola/Console/Command/OrganizationsAddFeature.php index 4e60bd2..82d3139 100644 --- a/src/Keboola/Console/Command/OrganizationsAddFeature.php +++ b/src/Keboola/Console/Command/OrganizationsAddFeature.php @@ -2,6 +2,7 @@ namespace Keboola\Console\Command; +use Keboola\ManageApi\ClientException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -50,29 +51,41 @@ public function execute(InputInterface $input, OutputInterface $output): ?int return 1; } + $failedOrgs = []; + $successFullOrgs = []; $orgIds = array_filter(explode(',', $orgIDsArg), 'is_numeric'); foreach ($orgIds as $orgId) { - $orgDetail = $client->getOrganization($orgId); + try { + $orgDetail = $client->getOrganization($orgId); + } catch (ClientException $e) { + $output->writeln(sprintf('ERROR: Cannot proceed org "%s" due "%s"', $orgId, $e->getMessage())); + $failedOrgs[] = $orgId; + } $output->writeln(sprintf('Adding feature to organization "%s" ("%s")', $orgDetail['id'], $orgDetail['name'])); $projectIds = array_map(fn($prj) => $prj['id'], $orgDetail['projects']); $this->addFeatureToSelectedProjects($client, $output, $featureName, $projectIds, $force); + $successFullOrgs[] = $orgId; } $output->writeln("\nDONE with following results:\n"); - $this->printResult($output, $force); + $this->printResult($output, $force, $successFullOrgs, $failedOrgs); return 0; } - private function printResult(OutputInterface $output, bool $force): void + private function printResult(OutputInterface $output, bool $force, array $successFullOrgs, array $failedOrgs): void { + $failedOrgsString = (count($failedOrgs) > 0) ? \sprintf(' (%s)', implode(', ', $failedOrgs)) : ''; $output->writeln(sprintf( - "%d projects where disabled\n" + "Processed %d organizations and %s failed\n" + . "%d projects where disabled\n" . "%d projects have the feature already\n" . '%d ' . ($force ? "projects updated" : "projects can be updated in force mode") . "\n", + count($successFullOrgs), + count($failedOrgs) . $failedOrgsString, $this->projectsDisabled, $this->projectsWithFeature, - $this->projectsUpdated + $this->projectsUpdated, ) ); } From 46e76fef2315e2a5e5ecc09f683d22fc1318dfde Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Fri, 19 Dec 2025 15:14:09 +0100 Subject: [PATCH 5/8] inheritance... --- .../Console/Command/OrganizationsAddFeature.php | 14 -------------- src/Keboola/Console/Command/ProjectsAddFeature.php | 10 +++++----- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/Keboola/Console/Command/OrganizationsAddFeature.php b/src/Keboola/Console/Command/OrganizationsAddFeature.php index 82d3139..1ae1250 100644 --- a/src/Keboola/Console/Command/OrganizationsAddFeature.php +++ b/src/Keboola/Console/Command/OrganizationsAddFeature.php @@ -10,21 +10,7 @@ class OrganizationsAddFeature extends ProjectsAddFeature { - const string ARG_FEATURE = 'feature'; const string ARG_ORGANIZATIONS = 'organizations'; - const string ARG_URL = 'url'; - const string ARG_TOKEN = 'token'; - const string OPT_FORCE = 'force'; - - protected int $maintainersChecked = 0; - - protected int $orgsChecked = 0; - - protected int $projectsDisabled = 0; - - protected int $projectsWithFeature = 0; - - protected int $projectsUpdated = 0; protected function configure(): void { diff --git a/src/Keboola/Console/Command/ProjectsAddFeature.php b/src/Keboola/Console/Command/ProjectsAddFeature.php index 8d032b6..f9119a8 100644 --- a/src/Keboola/Console/Command/ProjectsAddFeature.php +++ b/src/Keboola/Console/Command/ProjectsAddFeature.php @@ -11,11 +11,11 @@ class ProjectsAddFeature extends Command { - const ARG_FEATURE = 'feature'; - const ARG_PROJECTS = 'projects'; - const ARG_URL = 'url'; - const ARG_TOKEN = 'token'; - const OPT_FORCE = 'force'; + const string ARG_FEATURE = 'feature'; + const string ARG_PROJECTS = 'projects'; + const string ARG_URL = 'url'; + const string ARG_TOKEN = 'token'; + const string OPT_FORCE = 'force'; protected int $maintainersChecked = 0; From ae64cc31f6fb73baf2951ae8cb232eec6ff7cf04 Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Fri, 19 Dec 2025 15:16:44 +0100 Subject: [PATCH 6/8] fix cs --- .../Console/Command/OrganizationsAddFeature.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Keboola/Console/Command/OrganizationsAddFeature.php b/src/Keboola/Console/Command/OrganizationsAddFeature.php index 1ae1250..fb7f7b9 100644 --- a/src/Keboola/Console/Command/OrganizationsAddFeature.php +++ b/src/Keboola/Console/Command/OrganizationsAddFeature.php @@ -63,16 +63,15 @@ private function printResult(OutputInterface $output, bool $force, array $succes { $failedOrgsString = (count($failedOrgs) > 0) ? \sprintf(' (%s)', implode(', ', $failedOrgs)) : ''; $output->writeln(sprintf( - "Processed %d organizations and %s failed\n" + "Processed %d organizations and %s failed\n" . "%d projects where disabled\n" . "%d projects have the feature already\n" . '%d ' . ($force ? "projects updated" : "projects can be updated in force mode") . "\n", - count($successFullOrgs), - count($failedOrgs) . $failedOrgsString, - $this->projectsDisabled, - $this->projectsWithFeature, - $this->projectsUpdated, - ) - ); + count($successFullOrgs), + count($failedOrgs) . $failedOrgsString, + $this->projectsDisabled, + $this->projectsWithFeature, + $this->projectsUpdated, + )); } } From 1455dc68012bbb32608ec601fd63c41f875f7963 Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Fri, 19 Dec 2025 15:23:19 +0100 Subject: [PATCH 7/8] fix stan --- .../Console/Command/OrganizationsAddFeature.php | 10 +++++++++- src/Keboola/Console/Command/ProjectsAddFeature.php | 1 - 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Keboola/Console/Command/OrganizationsAddFeature.php b/src/Keboola/Console/Command/OrganizationsAddFeature.php index fb7f7b9..1a04eec 100644 --- a/src/Keboola/Console/Command/OrganizationsAddFeature.php +++ b/src/Keboola/Console/Command/OrganizationsAddFeature.php @@ -46,8 +46,9 @@ public function execute(InputInterface $input, OutputInterface $output): ?int } catch (ClientException $e) { $output->writeln(sprintf('ERROR: Cannot proceed org "%s" due "%s"', $orgId, $e->getMessage())); $failedOrgs[] = $orgId; + continue; } - $output->writeln(sprintf('Adding feature to organization "%s" ("%s")', $orgDetail['id'], $orgDetail['name'])); + $output->writeln(sprintf('Adding feature to organization "%s" ("%s")', $orgId, $orgDetail['name'])); $projectIds = array_map(fn($prj) => $prj['id'], $orgDetail['projects']); $this->addFeatureToSelectedProjects($client, $output, $featureName, $projectIds, $force); $successFullOrgs[] = $orgId; @@ -59,6 +60,13 @@ public function execute(InputInterface $input, OutputInterface $output): ?int return 0; } + /** + * @param OutputInterface $output + * @param bool $force + * @param int[] $successFullOrgs + * @param int[] $failedOrgs + * @return void + */ private function printResult(OutputInterface $output, bool $force, array $successFullOrgs, array $failedOrgs): void { $failedOrgsString = (count($failedOrgs) > 0) ? \sprintf(' (%s)', implode(', ', $failedOrgs)) : ''; diff --git a/src/Keboola/Console/Command/ProjectsAddFeature.php b/src/Keboola/Console/Command/ProjectsAddFeature.php index f9119a8..9d0614e 100644 --- a/src/Keboola/Console/Command/ProjectsAddFeature.php +++ b/src/Keboola/Console/Command/ProjectsAddFeature.php @@ -53,7 +53,6 @@ protected function createClient(string $host, string $token): Client protected function addFeatureToProject(Client $client, OutputInterface $output, array $projectInfo, string $featureName, bool $force): void { $projectId = (string) $projectInfo['id']; - assert(is_string($projectId)); $projectId = is_numeric($projectId) ? (int) $projectId : (int) $projectId; $output->writeln("Adding feature to project " . $projectId); From 3ca836491f292cde8c1e39ae26329f908606e766 Mon Sep 17 00:00:00 2001 From: Jiri Semmler Date: Fri, 19 Dec 2025 15:36:53 +0100 Subject: [PATCH 8/8] fix stan --- .../Command/OrganizationsAddFeature.php | 4 +-- .../Console/Command/ProjectsAddFeature.php | 28 +++++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/Keboola/Console/Command/OrganizationsAddFeature.php b/src/Keboola/Console/Command/OrganizationsAddFeature.php index 1a04eec..75fd972 100644 --- a/src/Keboola/Console/Command/OrganizationsAddFeature.php +++ b/src/Keboola/Console/Command/OrganizationsAddFeature.php @@ -63,8 +63,8 @@ public function execute(InputInterface $input, OutputInterface $output): ?int /** * @param OutputInterface $output * @param bool $force - * @param int[] $successFullOrgs - * @param int[] $failedOrgs + * @param string[] $successFullOrgs + * @param string[] $failedOrgs * @return void */ private function printResult(OutputInterface $output, bool $force, array $successFullOrgs, array $failedOrgs): void diff --git a/src/Keboola/Console/Command/ProjectsAddFeature.php b/src/Keboola/Console/Command/ProjectsAddFeature.php index 9d0614e..7d9173a 100644 --- a/src/Keboola/Console/Command/ProjectsAddFeature.php +++ b/src/Keboola/Console/Command/ProjectsAddFeature.php @@ -48,25 +48,26 @@ protected function createClient(string $host, string $token): Client } /** - * @param array $projectInfo + * @param array{ + * id: int, + * isDisabled?: bool, + * disabled: array{reason: string}, + * features: string[] + * } $projectInfo */ protected function addFeatureToProject(Client $client, OutputInterface $output, array $projectInfo, string $featureName, bool $force): void { $projectId = (string) $projectInfo['id']; - $projectId = is_numeric($projectId) ? (int) $projectId : (int) $projectId; $output->writeln("Adding feature to project " . $projectId); // Disabled projects if (isset($projectInfo["isDisabled"]) && $projectInfo["isDisabled"]) { $disabled = $projectInfo["disabled"]; - assert(is_array($disabled)); $disabledReason = $disabled["reason"]; - assert(is_string($disabledReason)); $output->writeln(" - project disabled: " . $disabledReason); $this->projectsDisabled++; } else { $features = $projectInfo["features"]; - assert(is_array($features)); if (in_array($featureName, $features, true)) { $output->writeln(" - feature '{$featureName}' is already set."); $this->projectsWithFeature++; @@ -76,7 +77,6 @@ protected function addFeatureToProject(Client $client, OutputInterface $output, $output->writeln(" - feature '{$featureName}' successfully added."); } else { $projectIdForDisplay = $projectInfo['id']; - assert(is_string($projectIdForDisplay) || is_int($projectIdForDisplay)); $output->writeln(sprintf(' - feature "%s" DOES NOT exist in the project %s yet. Enable force mode with -f option', $featureName, $projectIdForDisplay)); } $this->projectsUpdated++; @@ -96,6 +96,14 @@ protected function addFeatureToAllProjects(Client $client, OutputInterface $outp $projects = $client->listOrganizationProjects($organization['id']); foreach ($projects as $project) { + /** + * @var array{ + * id: int, + * isDisabled?: bool, + * disabled: array{reason: string}, + * features: string[] + * } $project + */ $this->addFeatureToProject($client, $output, $project, $feature, $force); } } @@ -110,6 +118,14 @@ protected function addFeatureToSelectedProjects(Client $client, OutputInterface foreach ($projectIds as $projectId) { try { $project = $client->getProject($projectId); + /** + * @var array{ + * id: int, + * isDisabled?: bool, + * disabled: array{reason: string}, + * features: string[] + * } $project + */ $this->addFeatureToProject($client, $output, $project, $featureName, $force); } catch (ClientException $e) { $output->writeln("Error while handling project {$projectId} : " . $e->getMessage());