Skip to content

Commit 6483a87

Browse files
feature #705 Add support for multiple Docker Compose files (dunglas)
This PR was squashed before being merged into the 1.9-dev branch. Discussion ---------- Add support for multiple Docker Compose files Allows recipes to ship configuration for multiple Docker Compose files (e.g. `docker-compose.override.yml`, `docker-compose.production.yml`). This is convenient to provide different settings for the development and the production environment. This is a companion PR for dunglas/symfony-docker#73. Commits ------- 2a08de6 Add support for multiple Docker Compose files
2 parents 446f0df + 2a08de6 commit 6483a87

File tree

2 files changed

+127
-63
lines changed

2 files changed

+127
-63
lines changed

src/Configurator/DockerComposeConfigurator.php

Lines changed: 73 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -29,83 +29,100 @@ public function configure(Recipe $recipe, $config, Lock $lock, array $options =
2929
}
3030

3131
$rootDir = $this->options->get('root-dir');
32-
if (
33-
(
34-
!file_exists($dockerComposeFile = $rootDir.'/docker-compose.yml') &&
35-
!file_exists($dockerComposeFile = $rootDir.'/docker-compose.yaml')
36-
) || $this->isFileMarked($recipe, $dockerComposeFile)
37-
) {
38-
return;
39-
}
32+
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".
36+
if (
37+
(!file_exists($dockerComposeFile) && !file_exists($dockerComposeFile = substr($dockerComposeFile, 0, -2).'aml')) ||
38+
$this->isFileMarked($recipe, $dockerComposeFile)
39+
) {
40+
continue;
41+
}
4042

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

43-
$offset = 8;
44-
$node = null;
45-
$endAt = [];
46-
$lines = [];
47-
foreach (file($dockerComposeFile) as $i => $line) {
48-
$lines[] = $line;
49-
$ltrimedLine = ltrim($line, ' ');
45+
$offset = 8;
46+
$node = null;
47+
$endAt = [];
48+
$lines = [];
49+
foreach (file($dockerComposeFile) as $i => $line) {
50+
$lines[] = $line;
51+
$ltrimedLine = ltrim($line, ' ');
5052

51-
// Skip blank lines and comments
52-
if (('' !== $ltrimedLine && 0 === strpos($ltrimedLine, '#')) || '' === trim($line)) {
53-
continue;
54-
}
53+
// Skip blank lines and comments
54+
if (('' !== $ltrimedLine && 0 === strpos($ltrimedLine, '#')) || '' === trim($line)) {
55+
continue;
56+
}
5557

56-
// Extract Docker Compose keys (usually "services" and "volumes")
57-
if (!preg_match('/^[\'"]?([a-zA-Z0-9]+)[\'"]?:\s*$/', $line, $matches)) {
58-
// Detect indentation to use
59-
$offestLine = \strlen($line) - \strlen($ltrimedLine);
60-
if ($offset > $offestLine && 0 !== $offestLine) {
61-
$offset = $offestLine;
58+
// Extract Docker Compose keys (usually "services" and "volumes")
59+
if (!preg_match('/^[\'"]?([a-zA-Z0-9]+)[\'"]?:\s*$/', $line, $matches)) {
60+
// Detect indentation to use
61+
$offestLine = \strlen($line) - \strlen($ltrimedLine);
62+
if ($offset > $offestLine && 0 !== $offestLine) {
63+
$offset = $offestLine;
64+
}
65+
continue;
6266
}
63-
continue;
67+
68+
// Keep end in memory (check break line on previous line)
69+
$endAt[$node] = '' !== trim($lines[$i - 1]) ? $i : $i - 1;
70+
$node = $matches[1];
6471
}
72+
$endAt[$node] = \count($lines) + 1;
6573

66-
// Keep end in memory (check break line on previous line)
67-
$endAt[$node] = '' !== trim($lines[$i - 1]) ? $i : $i - 1;
68-
$node = $matches[1];
69-
}
70-
$endAt[$node] = \count($lines) + 1;
74+
foreach ($extra as $key => $value) {
75+
if (isset($endAt[$key])) {
76+
array_splice($lines, $endAt[$key], 0, $this->markData($recipe, $this->parse(1, $offset, $value)));
77+
continue;
78+
}
7179

72-
foreach ($config as $key => $value) {
73-
if (isset($endAt[$key])) {
74-
array_splice($lines, $endAt[$key], 0, $this->markData($recipe, $this->parse(1, $offset, $value)));
75-
continue;
80+
$lines[] = sprintf("\n%s:", $key);
81+
$lines[] = $this->markData($recipe, $this->parse(1, $offset, $value));
7682
}
7783

78-
$lines[] = sprintf("\n%s:", $key);
79-
$lines[] = $this->markData($recipe, $this->parse(1, $offset, $value));
84+
file_put_contents($dockerComposeFile, implode('', $lines));
8085
}
81-
82-
file_put_contents($dockerComposeFile, implode('', $lines));
8386
}
8487

8588
public function unconfigure(Recipe $recipe, $config, Lock $lock)
8689
{
8790
$rootDir = $this->options->get('root-dir');
88-
if (!file_exists($dockerCompose = $rootDir.'/docker-compose.yml') &&
89-
!file_exists($dockerCompose = $rootDir.'/docker-compose.yaml')
90-
) {
91-
return;
92-
}
91+
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')) {
94+
continue;
95+
}
9396

94-
$name = $recipe->getName();
95-
// Remove recipe and add break line
96-
$contents = preg_replace(sprintf('{%s+###> %s ###.*?###< %s ###%s+}s', "\n", $name, $name, "\n"), PHP_EOL.PHP_EOL, file_get_contents($dockerCompose), -1, $count);
97-
if (!$count) {
98-
return;
99-
}
97+
$name = $recipe->getName();
98+
// Remove recipe and add break line
99+
$contents = preg_replace(sprintf('{%s+###> %s ###.*?###< %s ###%s+}s', "\n", $name, $name, "\n"), PHP_EOL.PHP_EOL, file_get_contents($dockerComposeFile), -1, $count);
100+
if (!$count) {
101+
return;
102+
}
100103

101-
foreach ($config as $key => $value) {
102-
if (0 === preg_match(sprintf('{^%s:[ \t\r\n]*([ \t]+\w|#)}m', $key), $contents, $matches)) {
103-
$contents = preg_replace(sprintf('{\n?^%s:[ \t\r\n]*}sm', $key), '', $contents, -1, $count);
104+
foreach ($extra as $key => $value) {
105+
if (0 === preg_match(sprintf('{^%s:[ \t\r\n]*([ \t]+\w|#)}m', $key), $contents, $matches)) {
106+
$contents = preg_replace(sprintf('{\n?^%s:[ \t\r\n]*}sm', $key), '', $contents, -1, $count);
107+
}
104108
}
109+
110+
$this->write(sprintf('Removing Docker Compose entries from "%s"', $dockerComposeFile));
111+
file_put_contents($dockerComposeFile, ltrim($contents, "\n"));
112+
}
113+
}
114+
115+
/**
116+
* Normalizes the config and return the name of the main Docker Compose file if applicable.
117+
*/
118+
private function normalizeConfig(array $config): array
119+
{
120+
foreach ($config as $val) {
121+
// Support for the short syntax recipe syntax that modifies docker-compose.yml only
122+
return isset($val[0]) ? ['docker-compose.yml' => $config] : $config;
105123
}
106124

107-
$this->write(sprintf('Removing Docker Compose entries from %s', $dockerCompose));
108-
file_put_contents($dockerCompose, ltrim($contents, "\n"));
125+
return $config;
109126
}
110127

111128
private function parse($level, $indent, $services): string

tests/Configurator/DockerComposeConfiguratorTest.php

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ class DockerComposeConfiguratorTest extends TestCase
9292
'volumes' => ['db-data: {}'],
9393
];
9494

95+
const CONFIG_DB_MULTIPLE_FILES = [
96+
'docker-compose.yml' => self::CONFIG_DB,
97+
'docker-compose.override.yml' => self::CONFIG_DB,
98+
];
99+
95100
/** @var Recipe|\PHPUnit\Framework\MockObject\MockObject */
96101
private $recipeDb;
97102

@@ -129,6 +134,7 @@ protected function setUp(): void
129134
protected function tearDown(): void
130135
{
131136
@unlink(FLEX_TEST_DIR.'/docker-compose.yml');
137+
@unlink(FLEX_TEST_DIR.'/docker-compose.override.yml');
132138
@unlink(FLEX_TEST_DIR.'/docker-compose.yaml');
133139
}
134140

@@ -139,7 +145,7 @@ public function testConfigure()
139145

140146
$this->configurator->configure($this->recipeDb, self::CONFIG_DB, $this->lock);
141147

142-
$this->assertEquals(self::ORIGINAL_CONTENT.<<<'YAML'
148+
$this->assertStringEqualsFile($dockerComposeFile, self::ORIGINAL_CONTENT.<<<'YAML'
143149
144150
###> doctrine/doctrine-bundle ###
145151
db:
@@ -162,7 +168,7 @@ public function testConfigure()
162168
###< doctrine/doctrine-bundle ###
163169

164170
YAML
165-
, file_get_contents($dockerComposeFile));
171+
);
166172

167173
$this->configurator->unconfigure($this->recipeDb, self::CONFIG_DB, $this->lock);
168174
$this->assertEquals(self::ORIGINAL_CONTENT, file_get_contents($dockerComposeFile));
@@ -182,7 +188,7 @@ public function testConfigureFileWithExistingVolumes()
182188

183189
$this->configurator->configure($this->recipeDb, self::CONFIG_DB, $this->lock);
184190

185-
$this->assertEquals(self::ORIGINAL_CONTENT.<<<'YAML'
191+
$this->assertStringEqualsFile($dockerComposeFile, self::ORIGINAL_CONTENT.<<<'YAML'
186192
187193
###> doctrine/doctrine-bundle ###
188194
db:
@@ -207,7 +213,7 @@ public function testConfigureFileWithExistingVolumes()
207213
###< doctrine/doctrine-bundle ###
208214

209215
YAML
210-
, file_get_contents($dockerComposeFile));
216+
);
211217

212218
$this->configurator->unconfigure($this->recipeDb, self::CONFIG_DB, $this->lock);
213219
// Not the same original, we have an extra breaks line
@@ -267,7 +273,7 @@ public function testConfigureFileWithExistingMarks()
267273

268274
$this->configurator->configure($recipe, $config, $this->lock);
269275

270-
$this->assertEquals(self::ORIGINAL_CONTENT.<<<'YAML'
276+
$this->assertStringEqualsFile($dockerComposeFile, self::ORIGINAL_CONTENT.<<<'YAML'
271277
272278
###> doctrine/doctrine-bundle ###
273279
db:
@@ -302,7 +308,7 @@ public function testConfigureFileWithExistingMarks()
302308
###< doctrine/doctrine-bundle ###
303309

304310
YAML
305-
, file_get_contents($dockerComposeFile));
311+
);
306312

307313
$this->configurator->unconfigure($recipe, $config, $this->lock);
308314
$this->assertEquals($originalContent, file_get_contents($dockerComposeFile));
@@ -381,6 +387,47 @@ public function testUnconfigureFileWithManyMarks()
381387
$recipe->method('getName')->willReturn('symfony/messenger');
382388

383389
$this->configurator->unconfigure($this->recipeDb, self::CONFIG_DB, $this->lock);
384-
$this->assertEquals($contentWithoutDoctrine, file_get_contents($dockerComposeFile));
390+
$this->assertStringEqualsFile($dockerComposeFile, $contentWithoutDoctrine);
391+
}
392+
393+
public function testConfigureMultipleFiles()
394+
{
395+
$dockerComposeFile = FLEX_TEST_DIR.'/docker-compose.yml';
396+
file_put_contents($dockerComposeFile, self::ORIGINAL_CONTENT);
397+
$dockerComposeOverrideFile = FLEX_TEST_DIR.'/docker-compose.override.yml';
398+
file_put_contents($dockerComposeOverrideFile, self::ORIGINAL_CONTENT);
399+
400+
$this->configurator->configure($this->recipeDb, self::CONFIG_DB_MULTIPLE_FILES, $this->lock);
401+
402+
foreach ([$dockerComposeFile, $dockerComposeOverrideFile] as $file) {
403+
$this->assertStringEqualsFile($file, self::ORIGINAL_CONTENT.<<<'YAML'
404+
405+
###> doctrine/doctrine-bundle ###
406+
db:
407+
image: mariadb:10.3
408+
environment:
409+
- MYSQL_DATABASE=symfony
410+
# You should definitely change the password in production
411+
- MYSQL_PASSWORD=password
412+
- MYSQL_RANDOM_ROOT_PASSWORD=true
413+
- MYSQL_USER=symfony
414+
volumes:
415+
- db-data:/var/lib/mysql:rw
416+
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
417+
# - ./docker/db/data:/var/lib/mysql:rw
418+
###< doctrine/doctrine-bundle ###
419+
420+
volumes:
421+
###> doctrine/doctrine-bundle ###
422+
db-data: {}
423+
###< doctrine/doctrine-bundle ###
424+
425+
YAML
426+
);
427+
}
428+
429+
$this->configurator->unconfigure($this->recipeDb, self::CONFIG_DB_MULTIPLE_FILES, $this->lock);
430+
$this->assertStringEqualsFile($dockerComposeFile, self::ORIGINAL_CONTENT);
431+
$this->assertStringEqualsFile($dockerComposeOverrideFile, self::ORIGINAL_CONTENT);
385432
}
386433
}

0 commit comments

Comments
 (0)