Skip to content

Commit 56bd6ab

Browse files
feat: docker-compose v2 support (#339)
* Update configuration fixtures to version 2 * Cater for ps on non-existing containers docker-compose v2 returns an exit code of 1 when running ps on a non-existent container, when v1 returned 0 and an empty list. * Remove failing container test docker-compose v2 starts containers that depend on failed containers. This may be considered a bug or a feature. Either way, the only way to recover is to bring all containers down. * Cope with missing version in configuration docker-compose config doesn't always include the version number from the original configuration, so this can't be reliably used to know whether the services are under the services key or directly under the root. * Cope with new container naming convention docker-compose v2 now names containers in the form: ${container_id}_${name}-\d+ Previously, it used an underscore after the name. * More graceful handling of missing services docker-compose ps returns an exit code of 1 when a named service doesn't exist. Rather than trying to work out why the exit code is 1, get the service list first to see whether it's worth running ps with a service name. * Explain reasoning behind matching logic * More thoroughly test output capturing Test that output from containers is captured by docker-compose. v2 strangely strips newlines in certain circumstances.
1 parent c9e3ccc commit 56bd6ab

File tree

9 files changed

+118
-169
lines changed

9 files changed

+118
-169
lines changed

src/main/groovy/com/avast/gradle/dockercompose/ComposeConfigParser.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class ComposeConfigParser
1717
{
1818
Map<String, Object> parsed = new Yaml().load(composeConfigOutput)
1919
// if there is 'version' on top-level then information about services is in 'services' sub-tree
20-
Map<String, Object> services = (parsed.version ? parsed.services : parsed)
20+
Map<String, Object> services = (parsed.services ? parsed.services : parsed)
2121
Map<String, Set<String>> declaredServiceDependencies = services.collectEntries { [(it.key): getDirectServiceDependencies(it.value)] }
2222
services.keySet().collectEntries { [(it): calculateDependenciesFromGraph(it, declaredServiceDependencies)] }
2323
}

src/main/groovy/com/avast/gradle/dockercompose/ComposeExecutor.groovy

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,14 @@ class ComposeExecutor {
100100
}
101101

102102
Iterable<String> getContainerIds(String serviceName) {
103-
execute('ps', '-q', serviceName).readLines()
103+
// `docker-compose ps -q serviceName` returns an exit code of 1 when the service
104+
// doesn't exist. To guard against this, check the service list first.
105+
def services = execute('ps', '--services').readLines()
106+
if (services.contains(serviceName)) {
107+
return execute('ps', '-q', serviceName).readLines()
108+
}
109+
110+
return []
104111
}
105112

106113
void captureContainersOutput(Closure<Void> logMethod, String... services) {

src/main/groovy/com/avast/gradle/dockercompose/ComposeSettings.groovy

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -291,20 +291,22 @@ abstract class ComposeSettings {
291291
}
292292

293293
protected Map<String, Object> createEnvironmentVariables(String variableName, ContainerInfo ci) {
294+
def serviceName = variableName.replaceAll('-', '_')
294295
Map<String, Object> environmentVariables = [:]
295-
environmentVariables.put("${variableName}_HOST".toString(), ci.host)
296-
environmentVariables.put("${variableName}_CONTAINER_HOSTNAME".toString(), ci.containerHostname)
297-
ci.tcpPorts.each { environmentVariables.put("${variableName}_TCP_${it.key}".toString(), it.value) }
298-
ci.udpPorts.each { environmentVariables.put("${variableName}_UDP_${it.key}".toString(), it.value) }
296+
environmentVariables.put("${serviceName}_HOST".toString(), ci.host)
297+
environmentVariables.put("${serviceName}_CONTAINER_HOSTNAME".toString(), ci.containerHostname)
298+
ci.tcpPorts.each { environmentVariables.put("${serviceName}_TCP_${it.key}".toString(), it.value) }
299+
ci.udpPorts.each { environmentVariables.put("${serviceName}_UDP_${it.key}".toString(), it.value) }
299300
environmentVariables
300301
}
301302

302303
protected Map<String, Object> createSystemProperties(String variableName, ContainerInfo ci) {
304+
def serviceName = variableName.replaceAll('-', '_')
303305
Map<String, Object> systemProperties = [:]
304-
systemProperties.put("${variableName}.host".toString(), ci.host)
305-
systemProperties.put("${variableName}.containerHostname".toString(), ci.containerHostname)
306-
ci.tcpPorts.each { systemProperties.put("${variableName}.tcp.${it.key}".toString(), it.value) }
307-
ci.udpPorts.each { systemProperties.put("${variableName}.udp.${it.key}".toString(), it.value) }
306+
systemProperties.put("${serviceName}.host".toString(), ci.host)
307+
systemProperties.put("${serviceName}.containerHostname".toString(), ci.containerHostname)
308+
ci.tcpPorts.each { systemProperties.put("${serviceName}.tcp.${it.key}".toString(), it.value) }
309+
ci.udpPorts.each { systemProperties.put("${serviceName}.udp.${it.key}".toString(), it.value) }
308310
systemProperties
309311
}
310312

src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeUp.groovy

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,10 @@ class ComposeUp extends DefaultTask {
159159
logger.info("Will use $host as host of service $serviceName")
160160
def tcpPorts = settings.dockerExecutor.getTcpPortsMapping(serviceName, inspection, host)
161161
def udpPorts = settings.dockerExecutor.getUdpPortsMapping(serviceName, inspection, host)
162-
String instanceName = inspection.Name.find(/${serviceName}_\d+/) ?: inspection.Name - '/'
162+
// docker-compose v1 uses an underscore as a separator. v2 uses a hyphen.
163+
String instanceName = inspection.Name.find(/${serviceName}_\d+$/) ?:
164+
inspection.Name.find(/${serviceName}-\d+$/) ?:
165+
inspection.Name - '/'
163166
new ContainerInfo(
164167
instanceName: instanceName,
165168
serviceHost: host,

src/test/groovy/com/avast/gradle/dockercompose/CaptureOutputTest.groovy

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import spock.lang.Specification
1010
class CaptureOutputTest extends Specification {
1111

1212
private String composeFileContent = '''
13-
web:
14-
image: nginx:stable
15-
command: bash -c "echo -e 'heres some output\\nand some more' && sleep 5 && nginx -g 'daemon off;'"
16-
ports:
17-
- 80
13+
version: '2'
14+
services:
15+
web:
16+
image: nginx:stable
17+
command: bash -c "echo -e 'here is some output' && echo -e 'and some more' && sleep 5 && nginx -g 'daemon off;'"
18+
ports:
19+
- 80
1820
'''
1921

2022
def "captures container output to stdout"() {
@@ -34,7 +36,9 @@ class CaptureOutputTest extends Specification {
3436
f.project.tasks.composeUp.up()
3537
then:
3638
noExceptionThrown()
37-
stdout.toString().contains("web_1 | heres some output\nweb_1 | and some more")
39+
stdout.toString().contains("web_1 | here is some output\nweb_1 | and some more") ||
40+
(stdout.toString().contains("web-1 | here is some output") &&
41+
stdout.toString().contains("web-1 | and some more"))
3842
cleanup:
3943
f.project.tasks.composeDown.down()
4044
f.close()
@@ -48,7 +52,9 @@ class CaptureOutputTest extends Specification {
4852
f.project.tasks.composeUp.up()
4953
then:
5054
noExceptionThrown()
51-
logFile.text.contains("web_1 | heres some output\nweb_1 | and some more")
55+
logFile.text.contains("web_1 | here is some output\nweb_1 | and some more") ||
56+
(logFile.text.contains("web-1 | here is some output") &&
57+
logFile.text.contains("web-1 | and some more"))
5258
cleanup:
5359
f.project.tasks.composeDown.down()
5460
f.close()
@@ -62,7 +68,9 @@ class CaptureOutputTest extends Specification {
6268
f.project.tasks.composeUp.up()
6369
then:
6470
noExceptionThrown()
65-
logFile.text.contains("web_1 | heres some output\nweb_1 | and some more")
71+
logFile.text.contains("web_1 | here is some output\nweb_1 | and some more") ||
72+
(logFile.text.contains("web-1 | here is some output") &&
73+
logFile.text.contains("web-1 | and some more"))
6674
cleanup:
6775
f.project.tasks.composeDown.down()
6876
f.close()
@@ -77,7 +85,9 @@ class CaptureOutputTest extends Specification {
7785
then:
7886
noExceptionThrown()
7987
def logFile = logDir.toPath().resolve('web.log').toFile()
80-
logFile.text.contains("web_1 | heres some output\nweb_1 | and some more")
88+
logFile.text.contains("web_1 | here is some output\nweb_1 | and some more") ||
89+
(logFile.text.contains("web-1 | here is some output") &&
90+
logFile.text.contains("web-1 | and some more"))
8191
cleanup:
8292
f.project.tasks.composeDown.down()
8393
f.close()
Lines changed: 6 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,10 @@
11
package com.avast.gradle.dockercompose
22

3-
import org.gradle.api.tasks.testing.Test
43
import spock.lang.Shared
54
import spock.lang.Specification
65
import spock.lang.Unroll
76

87
class ComposeExecutorTest extends Specification {
9-
@Shared
10-
def composeV1_webMasterWithDeps =
11-
'''
12-
web0:
13-
image: nginx:stable
14-
ports:
15-
- 80
16-
web1:
17-
image: nginx:stable
18-
ports:
19-
- 80
20-
links:
21-
- web0
22-
webMaster:
23-
image: nginx:stable
24-
ports:
25-
- 80
26-
links:
27-
- web1
28-
'''
29-
308
@Shared
319
def composeV2_webMasterWithDeps =
3210
'''
@@ -40,38 +18,23 @@ class ComposeExecutorTest extends Specification {
4018
image: nginx:stable
4119
ports:
4220
- 80
43-
links:
21+
depends_on:
4422
- web0
4523
webMaster:
4624
image: nginx:stable
4725
ports:
4826
- 80
49-
links:
50-
- web1
51-
'''
52-
53-
@Shared
54-
def composeWithFailingContainer = '''
55-
version: '3.9'
56-
services:
57-
fail:
58-
image: nginx:stable
59-
command: bash -c "echo not so stable && exit 1"
60-
double_fail:
61-
image: hello-world
6227
depends_on:
63-
fail:
64-
condition: service_completed_successfully
28+
- web1
6529
'''
6630

6731
@Unroll
6832
def "getServiceNames calculates service names correctly when includeDependencies is #includeDependencies" () {
69-
def f = Fixture.custom(composeFile)
33+
def f = Fixture.custom(composeV2_webMasterWithDeps)
7034
f.project.plugins.apply 'java'
7135
f.project.dockerCompose.includeDependencies = includeDependencies
7236
f.project.dockerCompose.startedServices = ['webMaster']
7337
f.project.plugins.apply 'docker-compose'
74-
Test test = f.project.tasks.test as Test
7538

7639
when:
7740
def configuredServices = f.project.dockerCompose.composeExecutor.getServiceNames()
@@ -84,35 +47,8 @@ class ComposeExecutorTest extends Specification {
8447

8548
where:
8649
// test it for both compose file version 1 and 2
87-
includeDependencies | expectedServices | composeFile
88-
true | ["webMaster", "web0", "web1"] | composeV1_webMasterWithDeps
89-
false | ["webMaster"] | composeV1_webMasterWithDeps
90-
true | ["webMaster", "web0", "web1"] | composeV2_webMasterWithDeps
91-
false | ["webMaster"] | composeV2_webMasterWithDeps
92-
}
93-
94-
def "If composeUp fails, containers should be deleted depending on retainContainersOnStartupFailure setting"() {
95-
setup:
96-
def f = Fixture.custom(composeWithFailingContainer)
97-
f.project.plugins.apply 'java'
98-
f.project.dockerCompose.startedServices = ['fail', 'double_fail']
99-
f.project.dockerCompose.retainContainersOnStartupFailure = retain
100-
f.project.dockerCompose
101-
f.project.plugins.apply 'docker-compose'
102-
103-
when:
104-
f.project.tasks.composeUp.up()
105-
106-
then:
107-
thrown(RuntimeException)
108-
assert f.project.dockerCompose.composeExecutor.getContainerIds('fail').size() == (retain ? 1 : 0)
109-
assert f.project.dockerCompose.composeExecutor.getContainerIds('double_fail').isEmpty()
110-
111-
cleanup:
112-
f.project.tasks.composeDownForced.down()
113-
f.close()
114-
115-
where:
116-
retain << [true, false]
50+
includeDependencies | expectedServices
51+
true | ["webMaster", "web0", "web1"]
52+
false | ["webMaster"]
11753
}
11854
}

src/test/groovy/com/avast/gradle/dockercompose/CustomComposeFilesTest.groovy

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@ class CustomComposeFilesTest extends Specification {
77
def "can specify compose files to use"() {
88
def projectDir = File.createTempDir("gradle", "projectDir")
99
new File(projectDir, 'original.yml') << '''
10-
web:
11-
image: nginx:stable
12-
ports:
13-
- 80
10+
version: '2'
11+
services:
12+
web:
13+
image: nginx:stable
14+
ports:
15+
- 80
1416
'''
1517
new File(projectDir, 'override.yml') << '''
16-
web:
17-
ports:
18-
- 8080
18+
version: '2'
19+
services:
20+
web:
21+
ports:
22+
- 8080
1923
'''
2024
def project = ProjectBuilder.builder().withProjectDir(projectDir).build()
2125
project.plugins.apply 'docker-compose'
@@ -45,17 +49,21 @@ class CustomComposeFilesTest extends Specification {
4549
def "docker-compose.override.yml file honoured when no files specified"() {
4650
def projectDir = File.createTempDir("gradle", "projectDir")
4751
new File(projectDir, 'docker-compose.yml') << '''
48-
web:
49-
image: nginx:stable
52+
version: '2'
53+
services:
54+
web:
55+
image: nginx:stable
5056
'''
5157
new File(projectDir, 'docker-compose.override.yml') << '''
52-
web:
53-
ports:
54-
- 80
55-
devweb:
56-
image: nginx:stable
57-
ports:
58-
- 80
58+
version: '2'
59+
services:
60+
web:
61+
ports:
62+
- 80
63+
devweb:
64+
image: nginx:stable
65+
ports:
66+
- 80
5967
'''
6068
def project = ProjectBuilder.builder().withProjectDir(projectDir).build()
6169
project.plugins.apply 'docker-compose'
@@ -80,22 +88,28 @@ class CustomComposeFilesTest extends Specification {
8088
def "docker-compose.override.yml file ignored when files are specified"() {
8189
def projectDir = File.createTempDir("gradle", "projectDir")
8290
new File(projectDir, 'docker-compose.yml') << '''
83-
web:
84-
image: nginx:stable
91+
version: '2'
92+
services:
93+
web:
94+
image: nginx:stable
8595
'''
8696
new File(projectDir, 'docker-compose.override.yml') << '''
87-
web:
88-
ports:
89-
- 80
90-
devweb:
91-
image: nginx:stable
92-
ports:
93-
- 80
97+
version: '2'
98+
services:
99+
web:
100+
ports:
101+
- 80
102+
devweb:
103+
image: nginx:stable
104+
ports:
105+
- 80
94106
'''
95107
new File(projectDir, 'docker-compose.prod.yml') << '''
96-
web:
97-
ports:
98-
- 8080
108+
version: '2'
109+
services:
110+
web:
111+
ports:
112+
- 8080
99113
'''
100114
def project = ProjectBuilder.builder().withProjectDir(projectDir).build()
101115
project.plugins.apply 'docker-compose'

0 commit comments

Comments
 (0)