From 64a0c90635b48fd7085221aec52b31a5a1c01815 Mon Sep 17 00:00:00 2001 From: Quentin Dawans Date: Mon, 11 May 2026 12:32:25 +0200 Subject: [PATCH 1/4] 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/4] 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/4] 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/4] 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';