Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions legacy/src/Command/Resources/ResourcesGetCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
{
Expand Down Expand Up @@ -113,6 +115,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$cpuTypeOption = $input->getOption('cpu-type');
$autoscalingIndicator = '<comment>(A)</comment>';
$hasAutoscalingIndicator = false;
$hasObjectStorage = false;
foreach ($services as $name => $service) {
$properties = $service->getProperties();
if (!$this->table->formatIsMachineReadable() && !empty($autoscalingEnabled[$name])) {
Expand All @@ -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,
Expand Down Expand Up @@ -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()) {
Comment on lines +186 to 191
if ($hasAutoscalingIndicator) {
Expand Down
58 changes: 57 additions & 1 deletion legacy/src/Command/Resources/ResourcesSetCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ protected function configure(): void
. "\nItems are in the format <info>name:value</info> 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 <info>name:value</info> 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');

Expand Down Expand Up @@ -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'));
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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('');
}
Expand Down Expand Up @@ -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',
));
}
}

/**
Expand Down Expand Up @@ -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; <error>%s</error> is a %s.',
$serviceName,
$this->typeName($service),
));
}
$gb = (int) $value;
if ($gb != $value || $value < 0) {
throw new InvalidArgumentException(sprintf(
'Invalid object storage size <error>%s</error>: it must be a non-negative integer in GB.',
$value,
));
}
Comment on lines +601 to +607
// The API stores object storage in MiB. 1 GB is treated as 1024 MiB.
return $gb * 1024;
}

/**
* Validates a given profile size.
*
Expand Down
15 changes: 15 additions & 0 deletions legacy/src/Service/ResourcesUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
36 changes: 36 additions & 0 deletions legacy/tests/Service/ResourcesUtilTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Platformsh\Cli\Tests\Service;

use PHPUnit\Framework\TestCase;
use Platformsh\Cli\Service\ResourcesUtil;

class ResourcesUtilTest extends TestCase
{
public function testFormatObjectStorageGB(): void
{
$cases = [
// [mib, expected_gb_string, description]
[0, '0', 'zero'],
[1024, '1', '1 GB exact'],
[524288, '512', '512 GB exact'],
[10485760, '10240', '10 TiB exact'],
[1536, '1.5', '1.5 GB exact'],
[1280, '1.25', '1.25 GB exact'],
[1500, '1.46', 'fractional rounds to 2 decimals'],
[100, '0.1', 'sub-GB value rounds and trims'],
[1024.0, '1', 'float input, whole GB'],
[1536.0, '1.5', 'float input, half GB'],
];
foreach ($cases as $key => $case) {
[$mib, $expected, $description] = $case;
$this->assertSame(
$expected,
ResourcesUtil::formatObjectStorageGB($mib),
"case $key: $description",
);
}
}
}