Skip to content

Commit 6093d0d

Browse files
committed
feat: switch to using placeholders in Dockerfile and add runtime options to docker:create
1 parent d6cf27a commit 6093d0d

9 files changed

Lines changed: 230 additions & 35 deletions

File tree

src/Command/Docker/CreateDockerfileCommand.php

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ class CreateDockerfileCommand extends AbstractCommand implements LocalProjectCom
3232
*/
3333
public const NAME = 'docker:create';
3434

35+
/**
36+
* Default architecture used when none is configured.
37+
*/
38+
private const DEFAULT_ARCHITECTURE = 'arm64';
39+
40+
/**
41+
* Default PHP runtime tag used when none is configured.
42+
*/
43+
private const DEFAULT_PHP_TAG = 'php-74';
44+
3545
/**
3646
* The project Dockerfile.
3747
*
@@ -58,6 +68,8 @@ protected function configure()
5868
->setName(self::NAME)
5969
->setDescription('Create a new Dockerfile')
6070
->addArgument('environment', InputArgument::OPTIONAL, 'The name of the environment to create the Dockerfile for')
71+
->addOption('architecture', null, InputOption::VALUE_REQUIRED, 'Docker architecture (arm64 or x86_64)')
72+
->addOption('php', null, InputOption::VALUE_REQUIRED, 'PHP version tag or version (for example: php-83 or 8.3)')
6173
->addOption('configure-project', null, InputOption::VALUE_NONE, 'Configure project\'s ymir.yml file');
6274
}
6375

@@ -72,16 +84,13 @@ protected function perform()
7284
throw new InvalidInputException(sprintf('Environment "%s" not found in ymir.yml file', $environment));
7385
}
7486

75-
$message = 'Dockerfile created';
76-
77-
if (!empty($environment)) {
78-
$message .= sprintf(' for "<comment>%s</comment>" environment', $environment);
79-
}
80-
8187
if (!$this->dockerfile->exists($environment) || $this->output->confirm('Dockerfile already exists. Do you want to overwrite it?', false)) {
82-
$this->dockerfile->create($environment);
88+
$architecture = $this->resolveArchitecture($environment);
89+
$phpVersion = $this->resolvePhpVersion($environment);
8390

84-
$this->output->info($message);
91+
$this->dockerfile->create($architecture, $phpVersion, $environment);
92+
93+
$this->output->info($this->generateDockerfileCreatedMessage($architecture, $environment, $phpVersion));
8594
}
8695

8796
if (!$this->input->getBooleanOption('configure-project') && !$this->output->confirm('Would you also like to configure your project for container image deployment?')) {
@@ -98,4 +107,46 @@ protected function perform()
98107

99108
$this->getProjectConfiguration()->applyChangesToEnvironment($environment, $configurationChange);
100109
}
110+
111+
/**
112+
* Generate the success message after creating the Dockerfile.
113+
*/
114+
private function generateDockerfileCreatedMessage(string $architecture, string $environment, string $phpVersion): string
115+
{
116+
return sprintf('Created <comment>%s</comment> for PHP <comment>%s</comment> and <comment>%s</comment> architecture', Dockerfile::getFileName($environment), $phpVersion, $architecture);
117+
}
118+
119+
/**
120+
* Resolve the architecture used to generate the Dockerfile.
121+
*/
122+
private function resolveArchitecture(string $environment): string
123+
{
124+
$architecture = (string) $this->input->getStringOption('architecture');
125+
126+
if (empty($architecture) && !empty($environment)) {
127+
$architecture = $this->getProjectConfiguration()->getEnvironmentConfiguration($environment)->getArchitecture();
128+
} elseif (empty($architecture)) {
129+
$architecture = self::DEFAULT_ARCHITECTURE;
130+
}
131+
132+
if (!in_array($architecture, ['arm64', 'x86_64'], true)) {
133+
throw new InvalidInputException(sprintf('Invalid architecture "%s". Supported values are: arm64, x86_64', $architecture));
134+
}
135+
136+
return $architecture;
137+
}
138+
139+
/**
140+
* Resolve the PHP version used to generate the Dockerfile.
141+
*/
142+
private function resolvePhpVersion(string $environment): string
143+
{
144+
$phpVersion = (string) $this->input->getStringOption('php');
145+
146+
if (empty($phpVersion) && !empty($environment)) {
147+
$phpVersion = $this->getProjectConfiguration()->getEnvironmentConfiguration($environment)->getPhpVersion();
148+
}
149+
150+
return empty($phpVersion) ? self::DEFAULT_PHP_TAG : $phpVersion;
151+
}
101152
}

src/Dockerfile.php

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace Ymir\Cli;
1515

1616
use Symfony\Component\Filesystem\Filesystem;
17+
use Ymir\Cli\Exception\InvalidArgumentException;
1718
use Ymir\Cli\Exception\SystemException;
1819

1920
class Dockerfile
@@ -53,12 +54,22 @@ public function __construct(Filesystem $filesystem, string $projectDirectory, st
5354
}
5455
}
5556

57+
/**
58+
* Get the Dockerfile name for the given environment.
59+
*/
60+
public static function getFileName(string $environment = ''): string
61+
{
62+
return empty($environment) ? 'Dockerfile' : sprintf('%s.Dockerfile', $environment);
63+
}
64+
5665
/**
5766
* Create a new Dockerfile.
5867
*/
59-
public function create(string $environment = ''): void
68+
public function create(string $architecture, string $phpVersion, string $environment = ''): void
6069
{
61-
$this->filesystem->copy($this->dockerfileStubPath, $this->generateDockerfilePath($environment), true);
70+
$this->filesystem->dumpFile(
71+
$this->generateDockerfilePath($environment), $this->generateDockerfileContent($this->resolvePlatform($architecture), $this->resolveRuntimeImage($architecture), $this->normalizePhpTag($phpVersion))
72+
);
6273
}
6374

6475
/**
@@ -99,17 +110,29 @@ public function validate(string $environment, string $architecture = ''): void
99110
}
100111

101112
/**
102-
* Generate the path to the Dockerfile.
113+
* Generate Dockerfile content from the Dockerfile stub.
103114
*/
104-
private function generateDockerfilePath(string $environment = ''): string
115+
private function generateDockerfileContent(string $platform, string $runtimeImage, string $phpTag): string
105116
{
106-
$dockerfileName = 'Dockerfile';
117+
$content = file_get_contents($this->dockerfileStubPath);
107118

108-
if (!empty($environment)) {
109-
$dockerfileName = $environment.'.'.$dockerfileName;
119+
if (false === $content) {
120+
throw new SystemException('Unable to read "Dockerfile" stub file');
110121
}
111122

112-
return $this->projectDirectory.'/'.$dockerfileName;
123+
return strtr($content, [
124+
'__YMIR_DOCKER_PLATFORM__' => $platform,
125+
'__YMIR_DOCKER_RUNTIME_IMAGE__' => $runtimeImage,
126+
'__YMIR_DOCKER_PHP_TAG__' => $phpTag,
127+
]);
128+
}
129+
130+
/**
131+
* Generate the path to the Dockerfile.
132+
*/
133+
private function generateDockerfilePath(string $environment = ''): string
134+
{
135+
return sprintf('%s/%s', $this->projectDirectory, self::getFileName($environment));
113136
}
114137

115138
/**
@@ -125,4 +148,48 @@ private function getDockerfilePath(string $environment = ''): string
125148

126149
return $dockerfilePath;
127150
}
151+
152+
/**
153+
* Normalize a PHP version into a Docker tag.
154+
*/
155+
private function normalizePhpTag(string $phpVersion): string
156+
{
157+
if (empty($phpVersion)) {
158+
throw new InvalidArgumentException('Unable to generate Dockerfile because no PHP version was provided');
159+
}
160+
161+
$version = '';
162+
163+
if (1 === preg_match('/^php-(\d+)\.(\d+)$/', $phpVersion, $matches)) {
164+
$version = $matches[1].$matches[2];
165+
} elseif (1 === preg_match('/^php-(\d{2})$/', $phpVersion, $matches)) {
166+
$version = $matches[1];
167+
} elseif (1 === preg_match('/^(\d+)\.(\d+)$/', $phpVersion, $matches)) {
168+
$version = $matches[1].$matches[2];
169+
} elseif (1 === preg_match('/^(\d{2})$/', $phpVersion, $matches)) {
170+
$version = $matches[1];
171+
}
172+
173+
if (empty($version)) {
174+
throw new InvalidArgumentException(sprintf('Unable to generate Dockerfile because "%s" is not a valid PHP version', $phpVersion));
175+
}
176+
177+
return sprintf('php-%s', $version);
178+
}
179+
180+
/**
181+
* Resolve the Docker platform from architecture.
182+
*/
183+
private function resolvePlatform(string $architecture): string
184+
{
185+
return 'arm64' === $architecture ? 'linux/arm64' : 'linux/amd64';
186+
}
187+
188+
/**
189+
* Resolve the Docker runtime image from architecture.
190+
*/
191+
private function resolveRuntimeImage(string $architecture): string
192+
{
193+
return 'arm64' === $architecture ? 'ymirapp/arm-php-runtime' : 'ymirapp/php-runtime';
194+
}
128195
}

src/Project/EnvironmentConfiguration.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ public function getName(): string
128128
return $this->name;
129129
}
130130

131+
/**
132+
* Get the PHP version for the environment.
133+
*/
134+
public function getPhpVersion(): string
135+
{
136+
return Arr::get($this->configuration, 'php', '');
137+
}
138+
131139
/**
132140
* Check if the environment has build configuration.
133141
*/

src/Project/Initialization/DockerInitializationStep.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public function perform(ExecutionContext $context, array $projectRequirements):
5656
}
5757

5858
if (!$this->dockerfile->exists() || $output->confirm('A <comment>Dockerfile</comment> already exists in the project directory. Do you want to overwrite it?', false)) {
59-
$this->dockerfile->create();
59+
$this->dockerfile->create('arm64', 'php-74');
6060
}
6161

6262
if (!$this->dockerExecutable->isInstalled()) {

stubs/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM --platform=linux/arm64 ymirapp/arm-php-runtime:php-74
1+
FROM --platform=__YMIR_DOCKER_PLATFORM__ __YMIR_DOCKER_RUNTIME_IMAGE__:__YMIR_DOCKER_PHP_TAG__
22

33
ENTRYPOINT []
44

tests/Integration/Command/Docker/CreateDockerfileCommandTest.php

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,15 @@ protected function setUp(): void
4141
public function testCreateDockerfile(): void
4242
{
4343
$this->setupActiveTeam();
44-
$this->setupValidProject();
44+
$this->setupValidProject(1, 'project', ['production' => ['architecture' => 'x86_64', 'php' => '8.3']]);
4545

4646
$this->bootApplication([new CreateDockerfileCommand($this->apiClient, $this->createExecutionContextFactory(), $this->dockerfile)]);
4747

4848
$tester = $this->executeCommand(CreateDockerfileCommand::NAME, [], ['no']); // no configure project
4949

50-
$this->assertStringContainsString('Dockerfile created', $tester->getDisplay());
50+
$this->assertStringContainsString('Created Dockerfile for PHP 8.2 and arm64 architecture', $tester->getDisplay());
5151
$this->assertFileExists($this->tempDir.'/Dockerfile');
52+
$this->assertStringContainsString('FROM --platform=linux/arm64 ymirapp/arm-php-runtime:php-74', (string) file_get_contents($this->tempDir.'/Dockerfile'));
5253
}
5354

5455
public function testCreateDockerfileAndConfigureProjectWithOption(): void
@@ -60,7 +61,7 @@ public function testCreateDockerfileAndConfigureProjectWithOption(): void
6061

6162
$tester = $this->executeCommand(CreateDockerfileCommand::NAME, ['--configure-project' => true], []);
6263

63-
$this->assertStringContainsString('Dockerfile created', $tester->getDisplay());
64+
$this->assertStringContainsString('Created Dockerfile for PHP 8.2 and arm64 architecture', $tester->getDisplay());
6465
$this->assertFileExists($this->tempDir.'/Dockerfile');
6566

6667
$config = $this->projectConfiguration->getEnvironmentConfiguration('staging');
@@ -70,7 +71,7 @@ public function testCreateDockerfileAndConfigureProjectWithOption(): void
7071
public function testCreateDockerfileForEnvironment(): void
7172
{
7273
$this->setupActiveTeam();
73-
$project = $this->setupValidProject(1, 'project', ['staging' => []]);
74+
$project = $this->setupValidProject(1, 'project', ['staging' => ['architecture' => 'x86_64', 'php' => '8.1']]);
7475
$environment = EnvironmentFactory::create(['name' => 'staging', 'project' => $project]);
7576

7677
$this->apiClient->shouldReceive('getEnvironments')->with($project)->andReturn(new ResourceCollection([$environment]));
@@ -81,8 +82,37 @@ public function testCreateDockerfileForEnvironment(): void
8182

8283
$tester = $this->executeCommand(CreateDockerfileCommand::NAME, ['environment' => 'staging'], ['no']); // no configure project
8384

84-
$this->assertStringContainsString('Dockerfile created for "staging" environment', $tester->getDisplay());
85+
$this->assertStringContainsString('Created staging.Dockerfile for PHP 8.1 and x86_64 architecture', $tester->getDisplay());
8586
$this->assertFileExists($this->tempDir.'/staging.Dockerfile');
87+
$this->assertStringContainsString('FROM --platform=linux/amd64 ymirapp/php-runtime:php-81', (string) file_get_contents($this->tempDir.'/staging.Dockerfile'));
88+
}
89+
90+
public function testCreateDockerfileWithArchitectureOption(): void
91+
{
92+
$this->setupActiveTeam();
93+
$this->setupValidProject();
94+
95+
$this->bootApplication([new CreateDockerfileCommand($this->apiClient, $this->createExecutionContextFactory(), $this->dockerfile)]);
96+
97+
$tester = $this->executeCommand(CreateDockerfileCommand::NAME, ['--architecture' => 'x86_64'], ['no']);
98+
99+
$this->assertStringContainsString('Created Dockerfile for PHP 8.2 and x86_64 architecture', $tester->getDisplay());
100+
$this->assertFileExists($this->tempDir.'/Dockerfile');
101+
$this->assertStringContainsString('FROM --platform=linux/amd64 ymirapp/php-runtime:php-74', (string) file_get_contents($this->tempDir.'/Dockerfile'));
102+
}
103+
104+
public function testCreateDockerfileWithPhpOption(): void
105+
{
106+
$this->setupActiveTeam();
107+
$this->setupValidProject();
108+
109+
$this->bootApplication([new CreateDockerfileCommand($this->apiClient, $this->createExecutionContextFactory(), $this->dockerfile)]);
110+
111+
$tester = $this->executeCommand(CreateDockerfileCommand::NAME, ['--php' => '8.3'], ['no']);
112+
113+
$this->assertStringContainsString('Created Dockerfile for PHP 8.3 and arm64 architecture', $tester->getDisplay());
114+
$this->assertFileExists($this->tempDir.'/Dockerfile');
115+
$this->assertStringContainsString('FROM --platform=linux/arm64 ymirapp/arm-php-runtime:php-83', (string) file_get_contents($this->tempDir.'/Dockerfile'));
86116
}
87117

88118
public function testDoNotOverwriteExistingDockerfile(): void
@@ -111,7 +141,7 @@ public function testOverwriteExistingDockerfile(): void
111141

112142
$tester = $this->executeCommand(CreateDockerfileCommand::NAME, [], ['yes', 'no']); // overwrite yes, no configure project
113143

114-
$this->assertStringContainsString('Dockerfile created', $tester->getDisplay());
144+
$this->assertStringContainsString('Created Dockerfile for PHP 8.2 and arm64 architecture', $tester->getDisplay());
115145
$this->assertStringNotEqualsFile($this->tempDir.'/Dockerfile', 'old content');
116146
}
117147
}

0 commit comments

Comments
 (0)