Skip to content

Commit b4f00b2

Browse files
committed
feat: add initialization step for migrating vapor configuration
1 parent c10195e commit b4f00b2

File tree

9 files changed

+1104
-163
lines changed

9 files changed

+1104
-163
lines changed

src/Command/Laravel/MigrateVaporCommand.php

Lines changed: 29 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,18 @@
1414
namespace Ymir\Cli\Command\Laravel;
1515

1616
use Illuminate\Support\Collection;
17-
use Symfony\Component\Filesystem\Filesystem;
18-
use Symfony\Component\Yaml\Yaml;
1917
use Ymir\Cli\ApiClient;
2018
use Ymir\Cli\Command\AbstractCommand;
2119
use Ymir\Cli\Command\LocalProjectCommandInterface;
22-
use Ymir\Cli\Dockerfile;
23-
use Ymir\Cli\Exception\InvalidInputException;
2420
use Ymir\Cli\Exception\Project\UnsupportedProjectException;
21+
use Ymir\Cli\Exception\RuntimeException;
2522
use Ymir\Cli\ExecutionContextFactory;
23+
use Ymir\Cli\Laravel\VaporDockerfileMigrator;
2624
use Ymir\Cli\Project\Configuration\Laravel\VaporConfigurationChange;
2725
use Ymir\Cli\Project\EnvironmentConfiguration;
2826
use Ymir\Cli\Project\Type\LaravelProjectType;
2927
use Ymir\Cli\Support\Arr;
28+
use Ymir\Cli\YamlParser;
3029

3130
class MigrateVaporCommand extends AbstractCommand implements LocalProjectCommandInterface
3231
{
@@ -38,28 +37,28 @@ class MigrateVaporCommand extends AbstractCommand implements LocalProjectCommand
3837
public const NAME = 'laravel:vapor:migrate';
3938

4039
/**
41-
* The project Dockerfile service.
40+
* The Dockerfile migrator service.
4241
*
43-
* @var Dockerfile
42+
* @var VaporDockerfileMigrator
4443
*/
45-
private $dockerfile;
44+
private $vaporDockerfileMigrator;
4645

4746
/**
48-
* The file system.
47+
* The vapor configuration parser.
4948
*
50-
* @var Filesystem
49+
* @var YamlParser
5150
*/
52-
private $filesystem;
51+
private $yamlParser;
5352

5453
/**
5554
* Constructor.
5655
*/
57-
public function __construct(ApiClient $apiClient, ExecutionContextFactory $contextFactory, Dockerfile $dockerfile, Filesystem $filesystem)
56+
public function __construct(ApiClient $apiClient, ExecutionContextFactory $contextFactory, VaporDockerfileMigrator $vaporDockerfileMigrator, YamlParser $yamlParser)
5857
{
5958
parent::__construct($apiClient, $contextFactory);
6059

61-
$this->dockerfile = $dockerfile;
62-
$this->filesystem = $filesystem;
60+
$this->vaporDockerfileMigrator = $vaporDockerfileMigrator;
61+
$this->yamlParser = $yamlParser;
6362
}
6463

6564
/**
@@ -85,7 +84,7 @@ protected function perform()
8584
$vaporEnvironments = Arr::get($vaporConfiguration, 'environments');
8685

8786
if (!is_array($vaporEnvironments)) {
88-
throw new InvalidInputException('No valid "environments" key found in vapor.yml file');
87+
throw new RuntimeException('No valid "environments" key found in vapor.yml file');
8988
}
9089

9190
$matchedEnvironments = $this->getProjectConfiguration()->getEnvironments()->keys()->intersect(collect($vaporEnvironments)->keys())->values();
@@ -100,75 +99,24 @@ protected function perform()
10099

101100
$imageDeploymentEnvironmentConfigurations = $this->getImageDeploymentEnvironmentConfigurations($matchedEnvironments);
102101

103-
if (!$imageDeploymentEnvironmentConfigurations->isEmpty()) {
104-
$this->migrateDockerfiles($imageDeploymentEnvironmentConfigurations);
102+
if ($imageDeploymentEnvironmentConfigurations->isNotEmpty()) {
103+
$migrationResult = $this->migrateDockerfiles($imageDeploymentEnvironmentConfigurations);
104+
105+
collect($migrationResult['created_dockerfiles'])->each(function (array $createdDockerfile): void {
106+
$this->output->info($this->generateDockerfileCreatedMessage($createdDockerfile));
107+
});
105108
}
106109

107110
$this->output->info('Vapor configuration migrated into <comment>ymir.yml</comment> file for the following environment(s):');
108111
$this->output->list($matchedEnvironments);
109112
}
110113

111-
/**
112-
* Back up all relevant Dockerfiles before migration.
113-
*/
114-
private function backupDockerfiles(Collection $environmentConfigurations, bool $backupGlobalDockerfile): void
115-
{
116-
$dockerfilePaths = $environmentConfigurations
117-
->map(function (EnvironmentConfiguration $environmentConfiguration): string {
118-
return $this->generateDockerfilePath($environmentConfiguration->getName());
119-
})
120-
->unique();
121-
122-
if ($backupGlobalDockerfile) {
123-
$dockerfilePaths->push($this->generateDockerfilePath());
124-
}
125-
126-
$dockerfilePaths->filter(function (string $dockerfilePath): bool {
127-
return $this->filesystem->exists($dockerfilePath);
128-
})->each(function (string $dockerfilePath): void {
129-
$this->filesystem->rename($dockerfilePath, $dockerfilePath.'.bak', true);
130-
});
131-
}
132-
133-
/**
134-
* Create a Dockerfile.
135-
*/
136-
private function createDockerfile(EnvironmentConfiguration $environmentConfiguration, string $phpVersion, bool $globalDockerfile): void
137-
{
138-
$architecture = $environmentConfiguration->getArchitecture() ?: 'x86_64';
139-
$environment = $globalDockerfile ? '' : $environmentConfiguration->getName();
140-
141-
$this->dockerfile->create($architecture, $phpVersion, $environment);
142-
143-
$this->output->info($this->generateDockerfileCreatedMessage($architecture, $environment, $phpVersion));
144-
145-
if ($globalDockerfile) {
146-
$this->output->comment(sprintf('Using <comment>%s</comment> environment configuration', $environmentConfiguration->getName()));
147-
}
148-
}
149-
150114
/**
151115
* Generate the success message after creating the Dockerfile.
152116
*/
153-
private function generateDockerfileCreatedMessage(string $architecture, string $environment, string $phpVersion): string
154-
{
155-
return sprintf('Created <comment>%s</comment> for PHP <comment>%s</comment> and <comment>%s</comment> architecture', Dockerfile::getFileName($environment), $phpVersion, $architecture);
156-
}
157-
158-
/**
159-
* Generate the full Dockerfile path.
160-
*/
161-
private function generateDockerfilePath(string $environment = ''): string
162-
{
163-
return sprintf('%s/%s', $this->getProjectDirectory(), Dockerfile::getFileName($environment));
164-
}
165-
166-
/**
167-
* Get the fallback PHP version from environment or project type.
168-
*/
169-
private function getFallbackPhpVersion(EnvironmentConfiguration $environmentConfiguration): string
117+
private function generateDockerfileCreatedMessage(array $createdDockerfile): string
170118
{
171-
return empty($environmentConfiguration->getPhpVersion()) ? $this->getProjectConfiguration()->getProjectType()->getDefaultPhpVersion() : $environmentConfiguration->getPhpVersion();
119+
return sprintf('Created <comment>%s</comment> for PHP <comment>%s</comment> and <comment>%s</comment> architecture', $createdDockerfile['name'], $createdDockerfile['php_version'], $createdDockerfile['architecture']);
172120
}
173121

174122
/**
@@ -191,77 +139,22 @@ private function getImageDeploymentEnvironmentConfigurations(Collection $matched
191139
*/
192140
private function getVaporConfiguration(): array
193141
{
194-
$vaporConfigurationFilePath = $this->getProjectDirectory().'/vapor.yml';
195-
196-
if (!$this->filesystem->exists($vaporConfigurationFilePath)) {
197-
throw new InvalidInputException(sprintf('No vapor configuration file found at "%s"', $vaporConfigurationFilePath));
198-
}
199-
200-
try {
201-
$vaporConfiguration = Yaml::parse((string) file_get_contents($vaporConfigurationFilePath));
202-
} catch (\Throwable $exception) {
203-
throw new InvalidInputException(sprintf('Error parsing Vapor configuration file: %s', $exception->getMessage()));
204-
}
142+
$vaporConfiguration = $this->yamlParser->parse($this->getProjectDirectory().'/vapor.yml');
205143

206-
if (!is_array($vaporConfiguration)) {
207-
throw new InvalidInputException('Error parsing Vapor configuration file');
144+
if (null === $vaporConfiguration) {
145+
throw new RuntimeException('Unable to migrate Vapor configuration because "vapor.yml" is missing from the project directory');
208146
}
209147

210148
return $vaporConfiguration;
211149
}
212150

213151
/**
214-
* Migrate Dockerfiles for image deployment environments.
215-
*/
216-
private function migrateDockerfiles(Collection $environmentConfigurations): void
217-
{
218-
$phpVersions = $environmentConfigurations->mapWithKeys(function (EnvironmentConfiguration $environmentConfiguration): array {
219-
return [$environmentConfiguration->getName() => $this->resolvePhpVersion($environmentConfiguration, $this->generateDockerfilePath($environmentConfiguration->getName()))];
220-
});
221-
$createGlobalDockerfile = $this->output->confirm('Do you want to create one global <comment>Dockerfile</comment> for all image deployment environments?', false);
222-
$sourceEnvironmentConfiguration = $this->selectDockerfileSourceEnvironmentConfiguration($environmentConfigurations);
223-
$dockerfileConfigurations = $createGlobalDockerfile ? collect([$sourceEnvironmentConfiguration]) : $environmentConfigurations;
224-
225-
$this->backupDockerfiles($environmentConfigurations, $createGlobalDockerfile);
226-
227-
$dockerfileConfigurations->each(function (EnvironmentConfiguration $environmentConfiguration) use ($createGlobalDockerfile, $phpVersions): void {
228-
$this->createDockerfile($environmentConfiguration, (string) $phpVersions->get($environmentConfiguration->getName()), $createGlobalDockerfile);
229-
});
230-
}
231-
232-
/**
233-
* Resolve the PHP version from the existing Dockerfile content.
152+
* Migrate the Dockerfiles for image deployment environments.
234153
*/
235-
private function resolvePhpVersion(EnvironmentConfiguration $environmentConfiguration, string $dockerfilePath): string
154+
private function migrateDockerfiles(Collection $imageDeploymentEnvironmentConfigurations): array
236155
{
237-
if (!$this->filesystem->exists($dockerfilePath)) {
238-
return $this->getFallbackPhpVersion($environmentConfiguration);
239-
}
240-
241-
$dockerfileContent = (string) file_get_contents($dockerfilePath);
242-
243-
if (1 !== preg_match('/:php-?(?:(\d+)\.(\d+)|(\d{2,3}))/i', $dockerfileContent, $matches)) {
244-
return $this->getFallbackPhpVersion($environmentConfiguration);
245-
} elseif (!empty($matches[1]) && !empty($matches[2])) {
246-
return sprintf('%s.%s', $matches[1], $matches[2]);
247-
} elseif (empty($matches[3])) {
248-
return $this->getFallbackPhpVersion($environmentConfiguration);
249-
}
250-
251-
$version = $matches[3];
252-
253-
return 2 === strlen($version) ? sprintf('%s.%s', $version[0], $version[1]) : sprintf('%s.%s', $version[0], substr($version, 1));
254-
}
255-
256-
/**
257-
* Select the source environment for Dockerfile generation.
258-
*/
259-
private function selectDockerfileSourceEnvironmentConfiguration(Collection $environmentConfigurations): EnvironmentConfiguration
260-
{
261-
$productionEnvironmentConfiguration = $environmentConfigurations->first(function (EnvironmentConfiguration $environmentConfiguration): bool {
262-
return 'production' === $environmentConfiguration->getName();
263-
});
264-
265-
return $productionEnvironmentConfiguration instanceof EnvironmentConfiguration ? $productionEnvironmentConfiguration : $environmentConfigurations->first();
156+
return $this->output->confirm('Do you want to create one global <comment>Dockerfile</comment> for all image deployment environments?', false)
157+
? $this->vaporDockerfileMigrator->migrateGlobalDockerfile($imageDeploymentEnvironmentConfigurations, $this->getProjectDirectory(), $this->getProjectConfiguration()->getProjectType())
158+
: $this->vaporDockerfileMigrator->migrateEnvironmentDockerfiles($imageDeploymentEnvironmentConfigurations, $this->getProjectDirectory(), $this->getProjectConfiguration()->getProjectType());
266159
}
267160
}

src/Command/Project/InitializeProjectCommand.php

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -134,24 +134,13 @@ protected function perform()
134134
'name' => $name,
135135
'provider' => $provider,
136136
'region' => $region,
137-
'environments' => $environments->keys()->all(),
138137
];
139138

140-
$initializationConfigurationChanges = collect($projectType->getInitializationSteps())
141-
->map(function (string $stepClass) use ($projectRequirements): ?ConfigurationChangeInterface {
142-
return $this->initializationStepLocator->get($stepClass)->perform($this->getContext(), $projectRequirements);
143-
})
144-
->filter();
145-
146-
$environments = $environments->map(function (EnvironmentConfiguration $configuration) use ($initializationConfigurationChanges, $projectType): EnvironmentConfiguration {
147-
foreach ($initializationConfigurationChanges as $initializationConfigurationChange) {
148-
$configuration = $initializationConfigurationChange->apply($configuration, $projectType);
149-
}
150-
151-
return $configuration;
152-
});
139+
$environments = $this->performInitializationSteps($projectType, $projectRequirements, $environments);
153140

154-
$project = $this->provision(Project::class, $projectRequirements);
141+
$project = $this->provision(Project::class, array_merge($projectRequirements, [
142+
'environments' => $environments->keys()->all(),
143+
]));
155144

156145
if (!$project instanceof Project) {
157146
throw new RuntimeException(sprintf('Unable to provision the "<comment>%s</comment>" project', $name));
@@ -172,6 +161,20 @@ private function addProjectType(ProjectTypeInterface $projectType): void
172161
$this->projectTypes[] = $projectType;
173162
}
174163

164+
/**
165+
* Apply an initialization configuration change to all environments.
166+
*/
167+
private function applyInitializationConfigurationChange(?ConfigurationChangeInterface $configurationChange, Collection $environments, ProjectTypeInterface $projectType): Collection
168+
{
169+
if (null === $configurationChange) {
170+
return $environments;
171+
}
172+
173+
return $environments->map(function (EnvironmentConfiguration $configuration) use ($configurationChange, $projectType): EnvironmentConfiguration {
174+
return $configurationChange->apply($configuration, $projectType);
175+
});
176+
}
177+
175178
/**
176179
* Get the base environments configuration for the project.
177180
*/
@@ -181,4 +184,31 @@ private function getBaseEnvironmentsConfiguration(ProjectTypeInterface $projectT
181184
return [$environment => $projectType->generateEnvironmentConfiguration($environment)];
182185
});
183186
}
187+
188+
/**
189+
* Perform a single initialization step.
190+
*/
191+
private function performInitializationStep(string $stepClass, array $baseProjectRequirements, Collection $environments): ?ConfigurationChangeInterface
192+
{
193+
$projectRequirements = array_merge($baseProjectRequirements, [
194+
'environments' => $environments->keys()->all(),
195+
'environment_configurations' => $environments,
196+
]);
197+
198+
return $this->initializationStepLocator->get($stepClass)->perform($this->getContext(), $projectRequirements);
199+
}
200+
201+
/**
202+
* Perform all initialization steps for the given project type.
203+
*/
204+
private function performInitializationSteps(ProjectTypeInterface $projectType, array $baseProjectRequirements, Collection $environments): Collection
205+
{
206+
foreach ($projectType->getInitializationSteps() as $stepClass) {
207+
$configurationChange = $this->performInitializationStep($stepClass, $baseProjectRequirements, $environments);
208+
209+
$environments = $this->applyInitializationConfigurationChange($configurationChange, $environments, $projectType);
210+
}
211+
212+
return $environments;
213+
}
184214
}

0 commit comments

Comments
 (0)