Skip to content

Commit 781efaa

Browse files
feature #715 Improve Docker Compose file detection (dunglas)
This PR was squashed before being merged into the 1.9-dev branch. Discussion ---------- Improve Docker Compose file detection This PR implements the rules described in the Docker Compose manual: https://docs.docker.com/compose/reference/envvars/#compose_file (see also #705 (comment), cc @jycamier). Basically, the directory containing the Compose definition can be configured using the `COMPOSE_FILE` environment variable, and it isn't defined Flex will look for the Compose file (as `docker-compose itself`) in the project directory and then in each parent director. This will be especially helpful for API Platform, where the `docker-compose.yml` file is in the parent directory of the Symfony application (https://github.com/api-platform/api-platform/), but also for other monolithic Git repositories containing several applications and a `docker-compose.yaml` file at the root (which is very common and considered a best practice). There is a voluntary limitation regarding the `COMPOSE_FILE` environment variable: the name of the file **must** be the one defined in the recipe (with the `.yml` or `.yaml` extension). In other words, only the directory is configurable, not the file name (on purpose). Commits ------- d04b0b6 Improve Docker Compose file detection
2 parents 059f763 + d04b0b6 commit 781efaa

File tree

2 files changed

+167
-13
lines changed

2 files changed

+167
-13
lines changed

src/Configurator/DockerComposeConfigurator.php

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111

1212
namespace Symfony\Flex\Configurator;
1313

14+
use Composer\Composer;
15+
use Composer\IO\IOInterface;
16+
use Symfony\Component\Filesystem\Filesystem;
1417
use Symfony\Flex\Lock;
18+
use Symfony\Flex\Options;
1519
use Symfony\Flex\Recipe;
1620

1721
/**
@@ -21,6 +25,15 @@
2125
*/
2226
class DockerComposeConfigurator extends AbstractConfigurator
2327
{
28+
private $filesystem;
29+
30+
public function __construct(Composer $composer, IOInterface $io, Options $options)
31+
{
32+
parent::__construct($composer, $io, $options);
33+
34+
$this->filesystem = new Filesystem();
35+
}
36+
2437
public function configure(Recipe $recipe, $config, Lock $lock, array $options = [])
2538
{
2639
$installDocker = $this->composer->getPackage()->getExtra()['symfony']['docker'] ?? false;
@@ -30,17 +43,14 @@ public function configure(Recipe $recipe, $config, Lock $lock, array $options =
3043

3144
$rootDir = $this->options->get('root-dir');
3245
foreach ($this->normalizeConfig($config) as $file => $extra) {
33-
$dockerComposeFile = sprintf('%s/%s', $rootDir, $file);
34-
35-
// Test with the ".yaml" extension if the file doesn't end up with ".yml".
3646
if (
37-
(!file_exists($dockerComposeFile) && !file_exists($dockerComposeFile = substr($dockerComposeFile, 0, -2).'aml')) ||
47+
(null === $dockerComposeFile = $this->findDockerComposeFile($rootDir, $file)) ||
3848
$this->isFileMarked($recipe, $dockerComposeFile)
3949
) {
4050
continue;
4151
}
4252

43-
$this->write(sprintf('Adding Docker Compose entries to "%s"', $dockerComposeFile));
53+
$this->write(sprintf('Adding Docker Compose definitions to "%s"', $dockerComposeFile));
4454

4555
$offset = 8;
4656
$node = null;
@@ -83,14 +93,15 @@ public function configure(Recipe $recipe, $config, Lock $lock, array $options =
8393

8494
file_put_contents($dockerComposeFile, implode('', $lines));
8595
}
96+
97+
$this->write('Docker Compose definitions have been modified. Please run "docker-compose up --build" again to apply the changes.');
8698
}
8799

88100
public function unconfigure(Recipe $recipe, $config, Lock $lock)
89101
{
90102
$rootDir = $this->options->get('root-dir');
91103
foreach ($this->normalizeConfig($config) as $file => $extra) {
92-
$dockerComposeFile = sprintf('%s/%s', $rootDir, $file);
93-
if (!file_exists($dockerComposeFile) && !file_exists($dockerComposeFile = substr($dockerComposeFile, 0, -2).'aml')) {
104+
if (null === $dockerComposeFile = $this->findDockerComposeFile($rootDir, $file)) {
94105
continue;
95106
}
96107

@@ -110,6 +121,8 @@ public function unconfigure(Recipe $recipe, $config, Lock $lock)
110121
$this->write(sprintf('Removing Docker Compose entries from "%s"', $dockerComposeFile));
111122
file_put_contents($dockerComposeFile, ltrim($contents, "\n"));
112123
}
124+
125+
$this->write('Docker Compose definitions have been modified. Please run "docker-compose up" again to apply the changes.');
113126
}
114127

115128
/**
@@ -125,6 +138,49 @@ private function normalizeConfig(array $config): array
125138
return $config;
126139
}
127140

141+
/**
142+
* Finds the Docker Compose file according to these rules: https://docs.docker.com/compose/reference/envvars/#compose_file.
143+
*/
144+
private function findDockerComposeFile(string $rootDir, string $file): ?string
145+
{
146+
if (isset($_SERVER['COMPOSE_FILE'])) {
147+
$separator = $_SERVER['COMPOSE_PATH_SEPARATOR'] ?? ('\\' === \DIRECTORY_SEPARATOR ? ';' : ':');
148+
149+
$files = explode($separator, $_SERVER['COMPOSE_FILE']);
150+
foreach ($files as $f) {
151+
if ($file !== basename($f)) {
152+
continue;
153+
}
154+
155+
if (!$this->filesystem->isAbsolutePath($f)) {
156+
$f = realpath(sprintf('%s/%s', $rootDir, $f));
157+
}
158+
159+
if ($this->filesystem->exists($f)) {
160+
return $f;
161+
}
162+
}
163+
}
164+
165+
// COMPOSE_FILE not set, or doesn't contain the file we're looking for
166+
$dir = $rootDir;
167+
$previousDir = null;
168+
do {
169+
// Test with the ".yaml" extension if the file doesn't end up with ".yml".
170+
if (
171+
$this->filesystem->exists($dockerComposeFile = sprintf('%s/%s', $dir, $file)) ||
172+
$this->filesystem->exists($dockerComposeFile = substr($dockerComposeFile, 0, -2).'aml')
173+
) {
174+
return $dockerComposeFile;
175+
}
176+
177+
$previousDir = $dir;
178+
$dir = \dirname($dir);
179+
} while ($dir !== $previousDir);
180+
181+
return null;
182+
}
183+
128184
private function parse($level, $indent, $services): string
129185
{
130186
$line = '';

tests/Configurator/DockerComposeConfiguratorTest.php

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Composer\IO\IOInterface;
1616
use Composer\Package\Package;
1717
use PHPUnit\Framework\TestCase;
18+
use Symfony\Component\Filesystem\Filesystem;
1819
use Symfony\Flex\Configurator\DockerComposeConfigurator;
1920
use Symfony\Flex\Lock;
2021
use Symfony\Flex\Options;
@@ -103,6 +104,9 @@ class DockerComposeConfiguratorTest extends TestCase
103104
/** @var Lock|\PHPUnit\Framework\MockObject\MockObject */
104105
private $lock;
105106

107+
/** @var Composer|\PHPUnit\Framework\MockObject\MockObject */
108+
private $composer;
109+
106110
/** @var DockerComposeConfigurator */
107111
private $configurator;
108112

@@ -121,21 +125,27 @@ protected function setUp(): void
121125
$package = new Package('dummy/dummy', '1.0.0', '1.0.0');
122126
$package->setExtra(['symfony' => ['docker' => true]]);
123127

124-
$composer = $this->getMockBuilder(Composer::class)->getMock();
125-
$composer->method('getPackage')->willReturn($package);
128+
$this->composer = $this->getMockBuilder(Composer::class)->getMock();
129+
$this->composer->method('getPackage')->willReturn($package);
126130

127131
$this->configurator = new DockerComposeConfigurator(
128-
$composer,
132+
$this->composer,
129133
$this->getMockBuilder(IOInterface::class)->getMock(),
130134
new Options(['config-dir' => 'config', 'root-dir' => FLEX_TEST_DIR])
131135
);
132136
}
133137

134138
protected function tearDown(): void
135139
{
136-
@unlink(FLEX_TEST_DIR.'/docker-compose.yml');
137-
@unlink(FLEX_TEST_DIR.'/docker-compose.override.yml');
138-
@unlink(FLEX_TEST_DIR.'/docker-compose.yaml');
140+
unset($_SERVER['COMPOSE_FILE']);
141+
142+
(new Filesystem())->remove([
143+
FLEX_TEST_DIR.'/docker-compose.yml',
144+
FLEX_TEST_DIR.'/docker-compose.override.yml',
145+
FLEX_TEST_DIR.'/docker-compose.yaml',
146+
FLEX_TEST_DIR.'/child/docker-compose.override.yaml',
147+
FLEX_TEST_DIR.'/child',
148+
]);
139149
}
140150

141151
public function testConfigure()
@@ -430,4 +440,92 @@ public function testConfigureMultipleFiles()
430440
$this->assertStringEqualsFile($dockerComposeFile, self::ORIGINAL_CONTENT);
431441
$this->assertStringEqualsFile($dockerComposeOverrideFile, self::ORIGINAL_CONTENT);
432442
}
443+
444+
public function testConfigureEnvVar()
445+
{
446+
@mkdir(FLEX_TEST_DIR.'/child/');
447+
$dockerComposeFile = FLEX_TEST_DIR.'/child/docker-compose.yml';
448+
file_put_contents($dockerComposeFile, self::ORIGINAL_CONTENT);
449+
$dockerComposeOverrideFile = FLEX_TEST_DIR.'/child/docker-compose.override.yml';
450+
file_put_contents($dockerComposeOverrideFile, self::ORIGINAL_CONTENT);
451+
452+
$sep = '\\' === \DIRECTORY_SEPARATOR ? ';' : ':';
453+
$_SERVER['COMPOSE_FILE'] = $dockerComposeFile.$sep.$dockerComposeOverrideFile;
454+
455+
$this->configurator->configure($this->recipeDb, self::CONFIG_DB_MULTIPLE_FILES, $this->lock);
456+
457+
foreach ([$dockerComposeFile, $dockerComposeOverrideFile] as $file) {
458+
$this->assertStringEqualsFile($file, self::ORIGINAL_CONTENT.<<<'YAML'
459+
460+
###> doctrine/doctrine-bundle ###
461+
db:
462+
image: mariadb:10.3
463+
environment:
464+
- MYSQL_DATABASE=symfony
465+
# You should definitely change the password in production
466+
- MYSQL_PASSWORD=password
467+
- MYSQL_RANDOM_ROOT_PASSWORD=true
468+
- MYSQL_USER=symfony
469+
volumes:
470+
- db-data:/var/lib/mysql:rw
471+
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
472+
# - ./docker/db/data:/var/lib/mysql:rw
473+
###< doctrine/doctrine-bundle ###
474+
475+
volumes:
476+
###> doctrine/doctrine-bundle ###
477+
db-data: {}
478+
###< doctrine/doctrine-bundle ###
479+
480+
YAML
481+
);
482+
}
483+
484+
$this->configurator->unconfigure($this->recipeDb, self::CONFIG_DB_MULTIPLE_FILES, $this->lock);
485+
$this->assertStringEqualsFile($dockerComposeFile, self::ORIGINAL_CONTENT);
486+
$this->assertStringEqualsFile($dockerComposeOverrideFile, self::ORIGINAL_CONTENT);
487+
}
488+
489+
public function testConfigureFileInParentDir()
490+
{
491+
$this->configurator = new DockerComposeConfigurator(
492+
$this->composer,
493+
$this->getMockBuilder(IOInterface::class)->getMock(),
494+
new Options(['config-dir' => 'config', 'root-dir' => FLEX_TEST_DIR.'/child'])
495+
);
496+
497+
@mkdir(FLEX_TEST_DIR.'/child');
498+
$dockerComposeFile = FLEX_TEST_DIR.'/docker-compose.yaml';
499+
file_put_contents($dockerComposeFile, self::ORIGINAL_CONTENT);
500+
501+
$this->configurator->configure($this->recipeDb, self::CONFIG_DB, $this->lock);
502+
503+
$this->assertStringEqualsFile($dockerComposeFile, self::ORIGINAL_CONTENT.<<<'YAML'
504+
505+
###> doctrine/doctrine-bundle ###
506+
db:
507+
image: mariadb:10.3
508+
environment:
509+
- MYSQL_DATABASE=symfony
510+
# You should definitely change the password in production
511+
- MYSQL_PASSWORD=password
512+
- MYSQL_RANDOM_ROOT_PASSWORD=true
513+
- MYSQL_USER=symfony
514+
volumes:
515+
- db-data:/var/lib/mysql:rw
516+
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
517+
# - ./docker/db/data:/var/lib/mysql:rw
518+
###< doctrine/doctrine-bundle ###
519+
520+
volumes:
521+
###> doctrine/doctrine-bundle ###
522+
db-data: {}
523+
###< doctrine/doctrine-bundle ###
524+
525+
YAML
526+
);
527+
528+
$this->configurator->unconfigure($this->recipeDb, self::CONFIG_DB, $this->lock);
529+
$this->assertEquals(self::ORIGINAL_CONTENT, file_get_contents($dockerComposeFile));
530+
}
433531
}

0 commit comments

Comments
 (0)