From 64a0c90635b48fd7085221aec52b31a5a1c01815 Mon Sep 17 00:00:00 2001 From: Quentin Dawans Date: Mon, 11 May 2026 12:32:25 +0200 Subject: [PATCH 1/5] feat(resources): add --object-storage option for apps Expose the new resources.disk.object field recently added on the platform side. resources:set accepts --object-storage name:value (non-negative integer GB, apps only); resources:get adds an "Object storage (GB)" column. Values are stored as MiB on the wire (1 GB = 1024 MiB) and converted on the way in and out via a shared ResourcesUtil::formatObjectStorageGB() helper. Range and step constraints are deferred to the API. --- .../Command/Resources/ResourcesGetCommand.php | 13 ++++- .../Command/Resources/ResourcesSetCommand.php | 58 ++++++++++++++++++- legacy/src/Service/ResourcesUtil.php | 15 +++++ legacy/tests/Service/ResourcesUtilTest.php | 36 ++++++++++++ 4 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 legacy/tests/Service/ResourcesUtilTest.php diff --git a/legacy/src/Command/Resources/ResourcesGetCommand.php b/legacy/src/Command/Resources/ResourcesGetCommand.php index 74683613b..420ace2ff 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) { @@ -127,6 +129,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,6 +165,14 @@ 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']); + } + $row['instance_count'] = isset($properties['instance_count']) ? $this->propertyFormatter->format($properties['instance_count'], 'instance_count') : '1'; $rows[] = $row; diff --git a/legacy/src/Command/Resources/ResourcesSetCommand.php b/legacy/src/Command/Resources/ResourcesSetCommand.php index 6d76c6597..e638a8951 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 ((string) $gb !== $value || $gb < 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 62c38968d..71b49771a 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 000000000..f84ecc0e7 --- /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", + ); + } + } +} From e6bb1e8d8a14be70bce0f590bcd3fe09e9cca253 Mon Sep 17 00:00:00 2001 From: Quentin Dawans Date: Tue, 12 May 2026 13:53:08 +0200 Subject: [PATCH 2/5] fix(resources:get): hide object-storage column when no app has it set Object storage is opt-in and rarely configured, so the column was displaying "not set" or "N/A" for every row on most projects. Drop it from the default column list when no application has it configured. Users can still request it explicitly via --columns. --- legacy/src/Command/Resources/ResourcesGetCommand.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/legacy/src/Command/Resources/ResourcesGetCommand.php b/legacy/src/Command/Resources/ResourcesGetCommand.php index 420ace2ff..10aa7a15f 100644 --- a/legacy/src/Command/Resources/ResourcesGetCommand.php +++ b/legacy/src/Command/Resources/ResourcesGetCommand.php @@ -115,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])) { @@ -171,6 +172,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $row['object_storage'] = $notApplicable; } elseif (isset($properties['resources']['disk']['object'])) { $row['object_storage'] = ResourcesUtil::formatObjectStorageGB($properties['resources']['disk']['object']); + $hasObjectStorage = true; } $row['instance_count'] = isset($properties['instance_count']) ? $this->propertyFormatter->format($properties['instance_count'], 'instance_count') : '1'; @@ -178,7 +180,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $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) { From 241d24dde97ab5e635210659e2af92f7cad82c7e Mon Sep 17 00:00:00 2001 From: Quentin Dawans Date: Tue, 12 May 2026 14:39:08 +0200 Subject: [PATCH 3/5] fix(resources:set): accept any integer form for --object-storage Align the object-storage validator with the disk-size validator by using loose comparison ($gb != $value) instead of a strict string cast. Inputs like "01" are now accepted, matching the behavior of other size options, while non-integers such as "1.5" or "abc" are still rejected. --- legacy/src/Command/Resources/ResourcesSetCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legacy/src/Command/Resources/ResourcesSetCommand.php b/legacy/src/Command/Resources/ResourcesSetCommand.php index e638a8951..b618b6242 100644 --- a/legacy/src/Command/Resources/ResourcesSetCommand.php +++ b/legacy/src/Command/Resources/ResourcesSetCommand.php @@ -599,7 +599,7 @@ protected function validateObjectStorage(string $value, string $serviceName, Web )); } $gb = (int) $value; - if ((string) $gb !== $value || $gb < 0) { + if ($gb != $value || $value < 0) { throw new InvalidArgumentException(sprintf( 'Invalid object storage size %s: it must be a non-negative integer in GB.', $value, From 233f1896ed8db09b872cac54f0190ddb5e4f366f Mon Sep 17 00:00:00 2001 From: Quentin Dawans Date: Wed, 13 May 2026 16:53:04 +0200 Subject: [PATCH 4/5] fix(resources:get): hide object-storage column when all apps set it to 0 The previous fix only hid the column when no app had object_storage defined. Apps that explicitly set it to 0 still triggered the column to appear with a "0" value across the table. Only treat the column as applicable when at least one app has a non-zero object_storage value. Assisted-By: Claude --- legacy/src/Command/Resources/ResourcesGetCommand.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/legacy/src/Command/Resources/ResourcesGetCommand.php b/legacy/src/Command/Resources/ResourcesGetCommand.php index 10aa7a15f..b32aad855 100644 --- a/legacy/src/Command/Resources/ResourcesGetCommand.php +++ b/legacy/src/Command/Resources/ResourcesGetCommand.php @@ -172,7 +172,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $row['object_storage'] = $notApplicable; } elseif (isset($properties['resources']['disk']['object'])) { $row['object_storage'] = ResourcesUtil::formatObjectStorageGB($properties['resources']['disk']['object']); - $hasObjectStorage = true; + if ($properties['resources']['disk']['object'] > 0) { + $hasObjectStorage = true; + } } $row['instance_count'] = isset($properties['instance_count']) ? $this->propertyFormatter->format($properties['instance_count'], 'instance_count') : '1'; From 4380ab019e99284b0d0403cd26e81a9d3eaf823b Mon Sep 17 00:00:00 2001 From: Quentin Dawans Date: Tue, 26 May 2026 14:31:29 +0200 Subject: [PATCH 5/5] refactor(resources): use MB for object storage input and display Treat the --object-storage value as MB on input and in all output, in line with the API (which stores the value in MiB). Previously the CLI accepted the value in GB and multiplied by 1024, while resources:get divided by 1024 for display. Both ends now pass the value through unchanged. - Update help text, example and validator error message - Drop the now-unused ResourcesUtil::formatObjectStorageGB helper and its test - Switch the resources:get column header from "(GB)" to "(MB)" Assisted-By: Claude --- .../Command/Resources/ResourcesGetCommand.php | 7 ++-- .../Command/Resources/ResourcesSetCommand.php | 23 ++++++------ legacy/src/Service/ResourcesUtil.php | 15 -------- legacy/tests/Service/ResourcesUtilTest.php | 36 ------------------- 4 files changed, 13 insertions(+), 68 deletions(-) delete mode 100644 legacy/tests/Service/ResourcesUtilTest.php diff --git a/legacy/src/Command/Resources/ResourcesGetCommand.php b/legacy/src/Command/Resources/ResourcesGetCommand.php index b32aad855..1fdfea676 100644 --- a/legacy/src/Command/Resources/ResourcesGetCommand.php +++ b/legacy/src/Command/Resources/ResourcesGetCommand.php @@ -31,7 +31,7 @@ class ResourcesGetCommand extends ResourcesCommandBase 'cpu' => 'CPU', 'memory' => 'Memory (MB)', 'disk' => 'Disk (MB)', - 'object_storage' => 'Object storage (GB)', + 'object_storage' => 'Object storage (MB)', 'instance_count' => 'Instances', 'base_memory' => 'Base memory', 'memory_ratio' => 'Memory ratio', @@ -166,12 +166,11 @@ 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. + // Object storage is only available on apps. if (!$service instanceof WebApp) { $row['object_storage'] = $notApplicable; } elseif (isset($properties['resources']['disk']['object'])) { - $row['object_storage'] = ResourcesUtil::formatObjectStorageGB($properties['resources']['disk']['object']); + $row['object_storage'] = (string) $properties['resources']['disk']['object']; if ($properties['resources']['disk']['object'] > 0) { $hasObjectStorage = true; } diff --git a/legacy/src/Command/Resources/ResourcesSetCommand.php b/legacy/src/Command/Resources/ResourcesSetCommand.php index b618b6242..f867a1510 100644 --- a/legacy/src/Command/Resources/ResourcesSetCommand.php +++ b/legacy/src/Command/Resources/ResourcesSetCommand.php @@ -64,7 +64,7 @@ protected function configure(): void 'object-storage', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, - 'Set the object storage size (in GB) of apps.' + 'Set the object storage size (in MB) of apps.' . "\nItems are in the format name:value as above." . "\nOnly applicable to apps; a value of 0 disables the bucket.", ) @@ -96,7 +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('Give 524288 MB (512 GB) of object storage to the "backend" app', '--object-storage backend:524288'); $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')); } @@ -478,12 +478,10 @@ private function summarizeChangesPerService(string $name, WebApp|Worker|Service )); } 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', + $properties['resources']['disk']['object'] ?? null, + $updates['resources']['disk']['object'], + ' MB', )); } } @@ -585,7 +583,7 @@ protected function validateDiskSize(string $value, string $serviceName, WebApp|W } /** - * Validates a given object storage size, returning the value in MiB. + * Validates a given object storage size in MB. * * @throws InvalidArgumentException */ @@ -598,15 +596,14 @@ protected function validateObjectStorage(string $value, string $serviceName, Web $this->typeName($service), )); } - $gb = (int) $value; - if ($gb != $value || $value < 0) { + $size = (int) $value; + if ($size != $value || $value < 0) { throw new InvalidArgumentException(sprintf( - 'Invalid object storage size %s: it must be a non-negative integer in GB.', + 'Invalid object storage size %s: it must be a non-negative integer in MB.', $value, )); } - // The API stores object storage in MiB. 1 GB is treated as 1024 MiB. - return $gb * 1024; + return $size; } /** diff --git a/legacy/src/Service/ResourcesUtil.php b/legacy/src/Service/ResourcesUtil.php index 71b49771a..62c38968d 100644 --- a/legacy/src/Service/ResourcesUtil.php +++ b/legacy/src/Service/ResourcesUtil.php @@ -225,21 +225,6 @@ 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 deleted file mode 100644 index f84ecc0e7..000000000 --- a/legacy/tests/Service/ResourcesUtilTest.php +++ /dev/null @@ -1,36 +0,0 @@ - $case) { - [$mib, $expected, $description] = $case; - $this->assertSame( - $expected, - ResourcesUtil::formatObjectStorageGB($mib), - "case $key: $description", - ); - } - } -}