Skip to content

Commit 255834c

Browse files
committed
Add sizing API support via resources commands
1 parent f8fae39 commit 255834c

File tree

12 files changed

+1100
-32
lines changed

12 files changed

+1100
-32
lines changed

config-defaults.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,9 @@ api:
281281
# Whether the Metrics API is enabled.
282282
metrics: false
283283

284+
# Whether the Flexible Resources API (AKA sizing/scaling) is enabled.
285+
sizing: false
286+
284287
# Whether Git Push Options are enabled.
285288
git_push_options: false
286289

src/Application.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ protected function getCommands()
238238
$commands[] = new Command\Backup\BackupGetCommand();
239239
$commands[] = new Command\Backup\BackupListCommand();
240240
$commands[] = new Command\Backup\BackupRestoreCommand();
241+
$commands[] = new Command\Resources\ResourcesGetCommand();
242+
$commands[] = new Command\Resources\ResourcesSizeListCommand();
243+
$commands[] = new Command\Resources\ResourcesSetCommand();
241244
$commands[] = new Command\RuntimeOperation\ListCommand();
242245
$commands[] = new Command\RuntimeOperation\RunCommand();
243246
$commands[] = new Command\SourceOperation\ListCommand();

src/Command/App/AppListCommand.php

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -115,20 +115,31 @@ protected function execute(InputInterface $input, OutputInterface $output)
115115

116116
private function recommendOtherCommands(EnvironmentDeployment $deployment)
117117
{
118-
if ($deployment->services || $deployment->workers) {
119-
$this->stdErr->writeln('');
120-
}
118+
$lines = [];
119+
$executable = $this->config()->get('application.executable');
121120
if ($deployment->services) {
122-
$this->stdErr->writeln(sprintf(
121+
$lines[] = sprintf(
123122
'To list services, run: <info>%s services</info>',
124-
$this->config()->get('application.executable')
125-
));
123+
$executable
124+
);
126125
}
127126
if ($deployment->workers) {
128-
$this->stdErr->writeln(sprintf(
127+
$lines[] = sprintf(
129128
'To list workers, run: <info>%s workers</info>',
130-
$this->config()->get('application.executable')
131-
));
129+
$executable
130+
);
131+
}
132+
if ($info = $deployment->getProperty('project_info', false)) {
133+
if (!empty($info['settings']['sizing_api_enabled']) && $this->config()->get('api.sizing') && $this->config()->isCommandEnabled('resources:set')) {
134+
$lines[] = sprintf(
135+
"To configure resources, run: <info>%s resources:set</info>",
136+
$executable
137+
);
138+
}
139+
}
140+
if ($lines) {
141+
$this->stdErr->writeln('');
142+
$this->stdErr->writeln($lines);
132143
}
133144
}
134145
}

src/Command/Environment/EnvironmentPushCommand.php

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,13 +273,39 @@ protected function execute(InputInterface $input, OutputInterface $output)
273273
// Clear the environment cache after pushing.
274274
$this->api()->clearEnvironmentsCache($project->id);
275275

276-
// Check the push log for possible deployment error messages.
277276
$log = $process->getErrorOutput();
277+
278+
// Check the push log for services that need resources configured ("flexible resources").
279+
if (\strpos($log, 'Invalid deployment') !== false
280+
&& \strpos($log, 'Resources must be configured') !== false) {
281+
$this->stdErr->writeln('');
282+
$this->stdErr->writeln('The push completed but resources must be configured before deployment can succeed.');
283+
if ($this->config()->isCommandEnabled('resources:set')) {
284+
$cmd = 'resources:set';
285+
if ($input->getOption('project')) {
286+
$cmd .= ' -p ' . OsUtil::escapeShellArg($input->getOption('project'));
287+
}
288+
if ($input->getOption('target')) {
289+
$cmd .= ' -e ' . OsUtil::escapeShellArg($input->getOption('target'));
290+
} elseif ($input->getOption('environment')) {
291+
$cmd .= ' -e ' . OsUtil::escapeShellArg($input->getOption('environment'));
292+
}
293+
$this->stdErr->writeln('');
294+
$this->stdErr->writeln(sprintf(
295+
'Configure resources for the environment by running: <comment>%s %s</comment>',
296+
$this->config()->get('application.executable'),
297+
$cmd
298+
));
299+
}
300+
return self::PUSH_FAILURE_EXIT_CODE;
301+
}
302+
303+
// Check the push log for other possible deployment error messages.
278304
$messages = $this->config()->getWithDefault('detection.push_deploy_error_messages', []);
279305
foreach ($messages as $message) {
280306
if (\strpos($log, $message) !== false) {
281307
$this->stdErr->writeln('');
282-
$this->stdErr->writeln(\sprintf('The "git push" completed but there was a deployment error ("<error>%s</error>").', $message));
308+
$this->stdErr->writeln(\sprintf('The push completed but there was a deployment error ("<error>%s</error>").', $message));
283309

284310
return self::PUSH_FAILURE_EXIT_CODE;
285311
}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
<?php
2+
3+
namespace Platformsh\Cli\Command\Resources;
4+
5+
use Doctrine\Common\Cache\CacheProvider;
6+
use Platformsh\Cli\Command\CommandBase;
7+
use Platformsh\Cli\Console\ArrayArgument;
8+
use Platformsh\Cli\Console\ProgressMessage;
9+
use Platformsh\Cli\Util\Wildcard;
10+
use Platformsh\Client\Exception\EnvironmentStateException;
11+
use Platformsh\Client\Model\Deployment\EnvironmentDeployment;
12+
use Platformsh\Client\Model\Deployment\Service;
13+
use Platformsh\Client\Model\Deployment\WebApp;
14+
use Platformsh\Client\Model\Deployment\Worker;
15+
use Platformsh\Client\Model\Environment;
16+
use Platformsh\Client\Model\Project;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
19+
class ResourcesCommandBase extends CommandBase
20+
{
21+
private static $cachedNextDeployment = [];
22+
23+
public function isHidden()
24+
{
25+
return !$this->config()->get('api.sizing') || parent::isHidden();
26+
}
27+
28+
/**
29+
* Lists services in a deployment.
30+
*
31+
* @param EnvironmentDeployment $deployment
32+
*
33+
* @return array<string, WebApp||Worker|Service>
34+
* An array of services keyed by the service name.
35+
*/
36+
protected function allServices(EnvironmentDeployment $deployment)
37+
{
38+
$webapps = $deployment->webapps;
39+
$workers = $deployment->workers;
40+
$services = $deployment->services;
41+
ksort($webapps, SORT_STRING|SORT_FLAG_CASE);
42+
ksort($workers, SORT_STRING|SORT_FLAG_CASE);
43+
ksort($services, SORT_STRING|SORT_FLAG_CASE);
44+
return array_merge($webapps, $workers, $services);
45+
}
46+
47+
/**
48+
* Checks whether a service needs a persistent disk.
49+
*
50+
* @todo replace this when the API has support for finding which services need a disk property
51+
*
52+
* @param WebApp|Service|Worker $service
53+
* @return bool
54+
*/
55+
protected function needsDisk($service)
56+
{
57+
if ($service instanceof Worker) {
58+
// Workers use the disk of their parent app.
59+
return false;
60+
}
61+
$diskless = ['chrome_headless', 'memcached', 'redis'];
62+
if ($service instanceof Service && in_array($service->type, $diskless)) {
63+
return false;
64+
}
65+
return !empty($service->disk) || ($service instanceof Service || !empty($service->mounts));
66+
}
67+
68+
/**
69+
* Loads the next environment deployment and caches it statically.
70+
*
71+
* The static cache means it can be reused while running a sub-command.
72+
*
73+
* @param Environment $environment
74+
* @param bool $reset
75+
* @return EnvironmentDeployment
76+
*/
77+
protected function loadNextDeployment(Environment $environment, $reset = false)
78+
{
79+
$cacheKey = $environment->project . ':' . $environment->id;
80+
if (isset(self::$cachedNextDeployment[$cacheKey]) && !$reset) {
81+
return self::$cachedNextDeployment[$cacheKey];
82+
}
83+
$progress = new ProgressMessage($this->stdErr);
84+
try {
85+
$progress->show('Loading deployment information...');
86+
$next = $environment->getNextDeployment();
87+
if (!$next) {
88+
throw new EnvironmentStateException('No next deployment found', $environment);
89+
}
90+
} finally {
91+
$progress->done();
92+
}
93+
return self::$cachedNextDeployment[$cacheKey] = $next;
94+
}
95+
96+
/**
97+
* Checks if a project supports the Flexible Resources API, AKA Sizing API.
98+
*
99+
* @param Project $project
100+
* @param EnvironmentDeployment|null $deployment
101+
* @return bool
102+
*/
103+
protected function supportsSizingApi(Project $project, EnvironmentDeployment $deployment = null)
104+
{
105+
if (isset($deployment->project_info['settings'])) {
106+
return !empty($deployment->project_info['settings']['sizing_api_enabled']);
107+
}
108+
/** @var CacheProvider $cacheService */
109+
$cacheService = $this->getService('cache');
110+
$cacheKey = 'project-settings:' . $project->id;
111+
$cachedSettings = $cacheService->fetch($cacheKey);
112+
if (!empty($cachedSettings['sizing_api_enabled'])) {
113+
return true;
114+
}
115+
$httpClient = $this->api()->getHttpClient();
116+
$settings = $httpClient->get($project->getUri() . '/settings')->json();
117+
$cacheService->save($cacheKey, $settings, $this->config()->get('api.projects_ttl'));
118+
return !empty($settings['sizing_api_enabled']);
119+
}
120+
121+
/**
122+
* Filters a list of services according to the --service or --type options.
123+
*
124+
* @param array<string, WebApp|Service|Worker> $services
125+
* @param InputInterface $input
126+
*
127+
* @return WebApp[]|Service[]|Worker[]|false
128+
* False on error, or an array of services.
129+
*/
130+
protected function filterServices($services, InputInterface $input)
131+
{
132+
$selectedNames = [];
133+
134+
$requestedServices = ArrayArgument::getOption($input, 'service');
135+
if (!empty($requestedServices)) {
136+
$selectedNames = Wildcard::select(array_keys($services), $requestedServices);
137+
if (!$selectedNames) {
138+
$this->stdErr->writeln('No services were found matching the name(s): <error>' . implode('</error>, <error>', $requestedServices) . '</error>');
139+
return false;
140+
}
141+
$services = array_intersect_key($services, array_flip($selectedNames));
142+
}
143+
$requestedApps = ArrayArgument::getOption($input, 'app');
144+
if (!empty($requestedApps)) {
145+
$selectedNames = Wildcard::select(array_keys(array_filter($services, function ($s) { return $s instanceof WebApp; })), $requestedApps);
146+
if (!$selectedNames) {
147+
$this->stdErr->writeln('No applications were found matching the name(s): <error>' . implode('</error>, <error>', $requestedApps) . '</error>');
148+
return false;
149+
}
150+
$services = array_intersect_key($services, array_flip($selectedNames));
151+
}
152+
$requestedWorkers = ArrayArgument::getOption($input, 'worker');
153+
if (!empty($requestedWorkers)) {
154+
$selectedNames = Wildcard::select(array_keys(array_filter($services, function ($s) { return $s instanceof Worker; })), $requestedWorkers);
155+
if (!$selectedNames) {
156+
$this->stdErr->writeln('No workers were found matching the name(s): <error>' . implode('</error>, <error>', $requestedWorkers) . '</error>');
157+
return false;
158+
}
159+
$services = array_intersect_key($services, array_flip($selectedNames));
160+
}
161+
162+
if ($input->hasOption('type') && ($requestedTypes = ArrayArgument::getOption($input, 'type'))) {
163+
$byType = [];
164+
foreach ($services as $name => $service) {
165+
$type = $service->type;
166+
list($prefix) = explode(':', $service->type, 2);
167+
$byType[$type][] = $name;
168+
$byType[$prefix][] = $name;
169+
}
170+
$selectedTypes = Wildcard::select(array_keys($byType), $requestedTypes);
171+
if (!$selectedTypes) {
172+
$this->stdErr->writeln('No services were found matching the type(s): <error>' . implode('</error>, <error>', $requestedTypes) . '</error>');
173+
return false;
174+
}
175+
foreach ($selectedTypes as $selectedType) {
176+
$selectedNames = array_merge($selectedNames, $byType[$selectedType]);
177+
}
178+
$services = array_intersect_key($services, array_flip($selectedNames));
179+
}
180+
181+
return $services;
182+
}
183+
184+
/**
185+
* Returns container profile size info, given service properties.
186+
*
187+
* @param array $properties
188+
* The service properties (e.g. from $service->getProperties()).
189+
* @param array $containerProfiles
190+
* The list of container profiles (e.g. from
191+
* $deployment->container_profiles).
192+
*
193+
* @return array{'cpu': string, 'memory': string}|null
194+
*/
195+
protected function sizeInfo(array $properties, array $containerProfiles)
196+
{
197+
if (isset($properties['resources']['profile_size'])) {
198+
$size = $properties['resources']['profile_size'];
199+
$profile = $properties['container_profile'];
200+
if (isset($containerProfiles[$profile][$size])) {
201+
return $containerProfiles[$profile][$size];
202+
}
203+
}
204+
return null;
205+
}
206+
}

0 commit comments

Comments
 (0)