11package com.avast.gradle.dockercompose
22
3- import org.gradle.api.file.ProjectLayout
3+ import org.gradle.api.Project
4+ import org.gradle.api.file.DirectoryProperty
45import org.gradle.api.internal.file.FileOperations
5- import org.gradle.api.invocation.Gradle
66import org.gradle.api.logging.Logger
77import org.gradle.api.logging.Logging
8+ import org.gradle.api.provider.ListProperty
9+ import org.gradle.api.provider.MapProperty
10+ import org.gradle.api.provider.Property
11+ import org.gradle.api.provider.Provider
12+ import org.gradle.api.services.BuildService
13+ import org.gradle.api.services.BuildServiceParameters
814import org.gradle.internal.UncheckedException
915import org.gradle.process.ExecOperations
1016import org.gradle.process.ExecSpec
1117import org.gradle.util.VersionNumber
1218import org.yaml.snakeyaml.Yaml
1319
1420import javax.inject.Inject
21+ import java.lang.ref.WeakReference
22+ import java.util.concurrent.ConcurrentHashMap
1523import java.util.concurrent.Executors
1624
17- class ComposeExecutor {
18- private final ComposeSettings settings
19- private final ProjectLayout layout
20- private final ExecOperations exec
21- private final FileOperations fileOps
22- private final Gradle gradle
25+ abstract class ComposeExecutor implements BuildService<Parameters > , AutoCloseable {
26+ static interface Parameters extends BuildServiceParameters {
27+ abstract DirectoryProperty getProjectDirectory ()
28+ abstract ListProperty<String > getStartedServices ()
29+ abstract ListProperty<String > getUseComposeFiles ()
30+ abstract Property<Boolean > getIncludeDependencies ()
31+ abstract DirectoryProperty getDockerComposeWorkingDirectory ()
32+ abstract MapProperty<String , Object > getEnvironment ()
33+ abstract Property<String > getExecutable ()
34+ abstract Property<String > getProjectName ()
35+ abstract ListProperty<String > getComposeAdditionalArgs ()
36+ abstract Property<Boolean > getRemoveOrphans ()
37+ abstract MapProperty<String , Integer > getScale ()
38+ }
2339
24- private static final Logger logger = Logging . getLogger(ComposeExecutor . class);
40+ static Provider<ComposeExecutor > getInstance (Project project , ComposeSettings settings ) {
41+ String serviceId = " ${ ComposeExecutor.class.canonicalName} $project . path ${ settings.hashCode()} "
42+ return project. gradle. sharedServices. registerIfAbsent(serviceId, ComposeExecutor ) {
43+ it. parameters. projectDirectory. set(project. layout. projectDirectory)
44+ it. parameters. startedServices. set(settings. startedServices)
45+ it. parameters. useComposeFiles. set(settings. useComposeFiles)
46+ it. parameters. includeDependencies. set(settings. includeDependencies)
47+ it. parameters. dockerComposeWorkingDirectory. set(settings. dockerComposeWorkingDirectory)
48+ it. parameters. environment. set(settings. environment)
49+ it. parameters. executable. set(settings. executable)
50+ it. parameters. projectName. set(settings. projectName)
51+ it. parameters. composeAdditionalArgs. set(settings. composeAdditionalArgs)
52+ it. parameters. removeOrphans. set(settings. removeOrphans)
53+ it. parameters. scale. set(settings. scale)
54+ }
55+ }
2556
2657 @Inject
27- ComposeExecutor (ComposeSettings settings , ProjectLayout layout , ExecOperations exec , FileOperations fileOps , Gradle gradle ) {
28- this . settings = settings
29- this . layout = layout
30- this . exec = exec
31- this . fileOps = fileOps
32- this . gradle = gradle
33- }
58+ abstract ExecOperations getExec ()
59+
60+ @Inject
61+ abstract FileOperations getFileOps ()
62+
63+ private static final Logger logger = Logging . getLogger(ComposeExecutor . class);
3464
3565 void executeWithCustomOutputWithExitValue (OutputStream os , String ... args ) {
3666 executeWithCustomOutput(os, false , true , true , args)
@@ -41,23 +71,24 @@ class ComposeExecutor {
4171 }
4272
4373 void executeWithCustomOutput (OutputStream os , Boolean ignoreExitValue , Boolean noAnsi , Boolean captureStderr , String ... args ) {
44- def settings = this . settings
4574 def er = exec. exec { ExecSpec e ->
46- if (settings. dockerComposeWorkingDirectory. isPresent()) {
47- e. setWorkingDir(settings. dockerComposeWorkingDirectory. get(). asFile)
75+ if (parameters. dockerComposeWorkingDirectory. isPresent()) {
76+ e. setWorkingDir(parameters. dockerComposeWorkingDirectory. get(). asFile)
77+ } else {
78+ e. setWorkingDir(parameters. projectDirectory)
4879 }
49- e. environment = System . getenv() + settings . environment. get()
50- def finalArgs = [settings . executable. get()]
51- finalArgs. addAll(settings . composeAdditionalArgs. get())
80+ e. environment = System . getenv() + parameters . environment. get()
81+ def finalArgs = [parameters . executable. get()]
82+ finalArgs. addAll(parameters . composeAdditionalArgs. get())
5283 if (noAnsi) {
5384 if (version >= VersionNumber . parse(' 1.28.0' )) {
5485 finalArgs. addAll([' --ansi' , ' never' ])
5586 } else if (version >= VersionNumber . parse(' 1.16.0' )) {
5687 finalArgs. add(' --no-ansi' )
5788 }
5889 }
59- finalArgs. addAll(settings . useComposeFiles. get(). collectMany { [' -f' , it]. asCollection() })
60- String pn = settings . projectName
90+ finalArgs. addAll(parameters . useComposeFiles. get(). collectMany { [' -f' , it]. asCollection() })
91+ String pn = parameters . projectName. get()
6192 if (pn) {
6293 finalArgs. addAll([' -p' , pn])
6394 }
@@ -73,7 +104,7 @@ class ComposeExecutor {
73104 }
74105 if (! ignoreExitValue && er. exitValue != 0 ) {
75106 def stdout = os != null ? os. toString(). trim() : " N/A"
76- throw new RuntimeException (" Exit-code ${ er.exitValue} when calling ${ settings .executable.get()} , stdout: $stdout " )
107+ throw new RuntimeException (" Exit-code ${ er.exitValue} when calling ${ parameters .executable.get()} , stdout: $stdout " )
77108 }
78109 }
79110
@@ -110,6 +141,8 @@ class ComposeExecutor {
110141 return []
111142 }
112143
144+ private Set<WeakReference<Thread > > threadsToInterruptOnClose = ConcurrentHashMap . newKeySet()
145+
113146 void captureContainersOutput (Closure<Void > logMethod , String ... services ) {
114147 // execute daemon thread that executes `docker-compose logs -f --no-color`
115148 // the -f arguments means `follow` and so this command ends when docker-compose finishes
@@ -152,24 +185,34 @@ class ComposeExecutor {
152185 })
153186 t. daemon = true
154187 t. start()
155- gradle. buildFinished { t. interrupt() }
188+ threadsToInterruptOnClose. add(new WeakReference<Thread > (t))
189+ }
190+
191+ @Override
192+ void close () throws Exception {
193+ threadsToInterruptOnClose. forEach {threadRef ->
194+ def thread = threadRef. get()
195+ if (thread != null ) {
196+ thread. interrupt()
197+ }
198+ }
156199 }
157200
158201 Iterable<String > getServiceNames () {
159- if (! settings . startedServices. get(). empty) {
160- if (settings . includeDependencies. get())
202+ if (! parameters . startedServices. get(). empty) {
203+ if (parameters . includeDependencies. get())
161204 {
162- def dependentServices = getDependentServices(settings . startedServices. get()). toList()
163- [* settings . startedServices. get(), * dependentServices]. unique()
205+ def dependentServices = getDependentServices(parameters . startedServices. get()). toList()
206+ [* parameters . startedServices. get(), * dependentServices]. unique()
164207 }
165208 else
166209 {
167- settings . startedServices. get()
210+ parameters . startedServices. get()
168211 }
169212 } else if (version >= VersionNumber . parse(' 1.6.0' )) {
170213 execute(' config' , ' --services' ). readLines()
171214 } else {
172- def composeFiles = settings . useComposeFiles. get(). empty ? getStandardComposeFiles() : getCustomComposeFiles()
215+ def composeFiles = parameters . useComposeFiles. get(). empty ? getStandardComposeFiles() : getCustomComposeFiles()
173216 composeFiles. collectMany { composeFile ->
174217 def compose = (Map<String , Object > ) (new Yaml (). load(fileOps. file(composeFile). text))
175218 // if there is 'version' on top-level then information about services is in 'services' sub-tree
@@ -190,7 +233,7 @@ class ComposeExecutor {
190233 }
191234
192235 Iterable<File > getStandardComposeFiles () {
193- File searchDirectory = fileOps. file(settings . dockerComposeWorkingDirectory) ?: layout . projectDirectory. getAsFile()
236+ File searchDirectory = fileOps. file(parameters . dockerComposeWorkingDirectory) ?: parameters . projectDirectory. getAsFile()
194237 def res = []
195238 def f = findInParentDirectories(' docker-compose.yml' , searchDirectory)
196239 if (f != null ) res. add(f)
@@ -200,7 +243,7 @@ class ComposeExecutor {
200243 }
201244
202245 Iterable<File > getCustomComposeFiles () {
203- settings . useComposeFiles. get(). collect {
246+ parameters . useComposeFiles. get(). collect {
204247 def f = fileOps. file(it)
205248 if (! f. exists()) {
206249 throw new IllegalArgumentException (" Custom Docker Compose file not found: $f " )
@@ -215,4 +258,15 @@ class ComposeExecutor {
215258 f. exists() ? f : findInParentDirectories(filename, directory. parentFile)
216259 }
217260
261+ boolean shouldRemoveOrphans () {
262+ version >= VersionNumber . parse(' 1.7.0' ) && parameters. removeOrphans. get()
263+ }
264+
265+ boolean isScaleSupported () {
266+ def v = version
267+ if (v < VersionNumber . parse(' 1.13.0' ) && parameters. scale) {
268+ throw new UnsupportedOperationException (" docker-compose version $v doesn't support --scale option" )
269+ }
270+ ! parameters. scale. get(). isEmpty()
271+ }
218272}
0 commit comments