diff --git a/legacy/src/Command/Resources/ResourcesGetCommand.php b/legacy/src/Command/Resources/ResourcesGetCommand.php index 74683613..b32aad85 100644 --- a/legacy/src/Command/Resources/ResourcesGetCommand.php +++ b/legacy/src/Command/Resources/ResourcesGetCommand.php @@ -11,6 +11,7 @@ use Platformsh\Cli\Service\PropertyFormatter; use Platformsh\Cli\Service\Table; use Platformsh\Client\Exception\EnvironmentStateException; +use Platformsh\Client\Model\Deployment\WebApp; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputInterface; @@ -30,13 +31,14 @@ class ResourcesGetCommand extends ResourcesCommandBase 'cpu' => 'CPU', 'memory' => 'Memory (MB)', 'disk' => 'Disk (MB)', + 'object_storage' => 'Object storage (GB)', 'instance_count' => 'Instances', 'base_memory' => 'Base memory', 'memory_ratio' => 'Memory ratio', ]; /** @var string[] */ - protected array $defaultColumns = ['service', 'profile_size', 'cpu_type', 'cpu', 'memory', 'disk', 'instance_count']; + protected array $defaultColumns = ['service', 'profile_size', 'cpu_type', 'cpu', 'memory', 'disk', 'object_storage', 'instance_count']; public function __construct(private readonly Api $api, private readonly Config $config, private readonly PropertyFormatter $propertyFormatter, private readonly ResourcesUtil $resourcesUtil, private readonly Selector $selector, private readonly Table $table) { @@ -113,6 +115,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $cpuTypeOption = $input->getOption('cpu-type'); $autoscalingIndicator = '(A)'; $hasAutoscalingIndicator = false; + $hasObjectStorage = false; foreach ($services as $name => $service) { $properties = $service->getProperties(); if (!$this->table->formatIsMachineReadable() && !empty($autoscalingEnabled[$name])) { @@ -127,6 +130,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'base_memory' => $empty, 'memory_ratio' => $empty, 'disk' => $empty, + 'object_storage' => $empty, 'instance_count' => $empty, 'cpu_type' => $empty, 'cpu' => $empty, @@ -162,12 +166,27 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } + // Object storage is only available on apps. Stored in MiB on the + // wire; displayed to users in GB. + if (!$service instanceof WebApp) { + $row['object_storage'] = $notApplicable; + } elseif (isset($properties['resources']['disk']['object'])) { + $row['object_storage'] = ResourcesUtil::formatObjectStorageGB($properties['resources']['disk']['object']); + if ($properties['resources']['disk']['object'] > 0) { + $hasObjectStorage = true; + } + } + $row['instance_count'] = isset($properties['instance_count']) ? $this->propertyFormatter->format($properties['instance_count'], 'instance_count') : '1'; $rows[] = $row; } - $this->table->render($rows, $this->tableHeader, $this->defaultColumns); + $defaultColumns = $this->defaultColumns; + if (!$hasObjectStorage) { + $defaultColumns = array_values(array_diff($defaultColumns, ['object_storage'])); + } + $this->table->render($rows, $this->tableHeader, $defaultColumns); if (!$this->table->formatIsMachineReadable()) { if ($hasAutoscalingIndicator) { diff --git a/legacy/src/Command/Resources/ResourcesSetCommand.php b/legacy/src/Command/Resources/ResourcesSetCommand.php index 6d76c659..b618b624 100644 --- a/legacy/src/Command/Resources/ResourcesSetCommand.php +++ b/legacy/src/Command/Resources/ResourcesSetCommand.php @@ -60,6 +60,14 @@ protected function configure(): void . "\nItems are in the format name:value as above." . "\nA value of 'default' will use the default size, and 'min' or 'minimum' will use the minimum.", ) + ->addOption( + 'object-storage', + null, + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Set the object storage size (in GB) of apps.' + . "\nItems are in the format name:value as above." + . "\nOnly applicable to apps; a value of 0 disables the bucket.", + ) ->addOption('force', 'f', InputOption::VALUE_NONE, 'Try to run the update, even if it might exceed your limits') ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show the changes that would be made, without changing anything'); @@ -88,6 +96,7 @@ protected function configure(): void $this->addExample('Set profile sizes for two apps and a service', '--size frontend:0.1,backend:.25,database:1'); $this->addExample('Give the "backend" app 3 instances', '--count backend:3'); $this->addExample('Give 512 MB disk to the "backend" app and 2 GB to the "database" service', '--disk backend:512,database:2048'); + $this->addExample('Give 512 GB of object storage to the "backend" app', '--object-storage backend:512'); $this->addExample('Set the same profile size for the "backend" and "frontend" apps using a wildcard', '--size ' . OsUtil::escapeShellArg('*end:0.1')); $this->addExample('Set the same instance count for all apps using a wildcard', '--count ' . OsUtil::escapeShellArg('*:3')); } @@ -143,6 +152,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Validate the --disk option. [$givenDiskSizes, $diskErrored] = $this->parseSetting($input, 'disk', $services, fn($v, $serviceName, $service) => $this->validateDiskSize($v, $serviceName, $service)); $errored = $errored || $diskErrored; + + // Validate the --object-storage option. + [$givenObjectStorage, $objectStorageErrored] = $this->parseSetting($input, 'object-storage', $services, fn($v, $serviceName, $service) => $this->validateObjectStorage($v, $serviceName, $service)); + $errored = $errored || $objectStorageErrored; if ($errored) { return 1; } @@ -171,7 +184,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $showCompleteForm = $input->isInteractive() && $input->getOption('size') === [] && $input->getOption('count') === [] - && $input->getOption('disk') === []; + && $input->getOption('disk') === [] + && $input->getOption('object-storage') === []; $updates = []; $current = []; @@ -304,6 +318,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } + // Set the object storage size (apps only). + if ($service instanceof WebApp && isset($givenObjectStorage[$name])) { + $currentObject = $properties['resources']['disk']['object'] ?? null; + if ($givenObjectStorage[$name] !== $currentObject) { + $updates[$group][$name]['resources']['disk']['object'] = $givenObjectStorage[$name]; + } + } + if ($headerShown) { $this->stdErr->writeln(''); } @@ -455,6 +477,15 @@ private function summarizeChangesPerService(string $name, WebApp|Worker|Service ' MB', )); } + if (isset($updates['resources']['disk']['object'])) { + $previousMib = $properties['resources']['disk']['object'] ?? null; + $newMib = $updates['resources']['disk']['object']; + $this->stdErr->writeln(' Object storage: ' . $this->resourcesUtil->formatChange( + $previousMib === null ? null : ResourcesUtil::formatObjectStorageGB($previousMib), + ResourcesUtil::formatObjectStorageGB($newMib), + ' GB', + )); + } } /** @@ -553,6 +584,31 @@ protected function validateDiskSize(string $value, string $serviceName, WebApp|W return $size; } + /** + * Validates a given object storage size, returning the value in MiB. + * + * @throws InvalidArgumentException + */ + protected function validateObjectStorage(string $value, string $serviceName, WebApp|Worker|Service $service): int + { + if (!$service instanceof WebApp) { + throw new InvalidArgumentException(sprintf( + 'Object storage is only available on apps; %s is a %s.', + $serviceName, + $this->typeName($service), + )); + } + $gb = (int) $value; + if ($gb != $value || $value < 0) { + throw new InvalidArgumentException(sprintf( + 'Invalid object storage size %s: it must be a non-negative integer in GB.', + $value, + )); + } + // The API stores object storage in MiB. 1 GB is treated as 1024 MiB. + return $gb * 1024; + } + /** * Validates a given profile size. * diff --git a/legacy/src/Service/ResourcesUtil.php b/legacy/src/Service/ResourcesUtil.php index 62c38968..71b49771 100644 --- a/legacy/src/Service/ResourcesUtil.php +++ b/legacy/src/Service/ResourcesUtil.php @@ -225,6 +225,21 @@ public function formatCPU(int|float|string $unformatted): string return sprintf('%.1f', $unformatted); } + /** + * Formats a MiB value as a GB string for object storage display. + * + * Object storage is in MiB on the wire; the CLI exposes it to users in GB + * (where 1 GB is treated as 1024 MiB). + */ + public static function formatObjectStorageGB(int|float $mib): string + { + $gb = $mib / 1024; + if ($gb == (int) $gb) { + return (string) (int) $gb; + } + return rtrim(rtrim(sprintf('%.2f', $gb), '0'), '.'); + } + /** * Adds a --resources-init option to commands that support it. * diff --git a/legacy/tests/Service/ResourcesUtilTest.php b/legacy/tests/Service/ResourcesUtilTest.php new file mode 100644 index 00000000..f84ecc0e --- /dev/null +++ b/legacy/tests/Service/ResourcesUtilTest.php @@ -0,0 +1,36 @@ + $case) { + [$mib, $expected, $description] = $case; + $this->assertSame( + $expected, + ResourcesUtil::formatObjectStorageGB($mib), + "case $key: $description", + ); + } + } +}