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 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..75fd972 --- /dev/null +++ b/src/Keboola/Console/Command/OrganizationsAddFeature.php @@ -0,0 +1,85 @@ +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; + } + + $failedOrgs = []; + $successFullOrgs = []; + $orgIds = array_filter(explode(',', $orgIDsArg), 'is_numeric'); + foreach ($orgIds as $orgId) { + try { + $orgDetail = $client->getOrganization($orgId); + } 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")', $orgId, $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, $successFullOrgs, $failedOrgs); + + return 0; + } + + /** + * @param OutputInterface $output + * @param bool $force + * @param string[] $successFullOrgs + * @param string[] $failedOrgs + * @return void + */ + private function printResult(OutputInterface $output, bool $force, array $successFullOrgs, array $failedOrgs): void + { + $failedOrgsString = (count($failedOrgs) > 0) ? \sprintf(' (%s)', implode(', ', $failedOrgs)) : ''; + $output->writeln(sprintf( + "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, + )); + } +} diff --git a/src/Keboola/Console/Command/ProjectsAddFeature.php b/src/Keboola/Console/Command/ProjectsAddFeature.php index 3c14df2..7d9173a 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; @@ -48,26 +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 = $projectInfo['id']; - assert(is_string($projectId)); - $projectId = is_numeric($projectId) ? (int) $projectId : (int) $projectId; + $projectId = (string) $projectInfo['id']; $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++; @@ -77,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++; @@ -97,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); } } @@ -111,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());