Skip to content

Commit baddaa5

Browse files
ALikhachevaugi
authored andcommitted
Refactor plugin to be compatible with Gradle configuration cache
Fixes #307
1 parent 01a2177 commit baddaa5

17 files changed

+549
-222
lines changed

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

Lines changed: 88 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,66 @@
11
package 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
45
import org.gradle.api.internal.file.FileOperations
5-
import org.gradle.api.invocation.Gradle
66
import org.gradle.api.logging.Logger
77
import 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
814
import org.gradle.internal.UncheckedException
915
import org.gradle.process.ExecOperations
1016
import org.gradle.process.ExecSpec
1117
import org.gradle.util.VersionNumber
1218
import org.yaml.snakeyaml.Yaml
1319

1420
import javax.inject.Inject
21+
import java.lang.ref.WeakReference
22+
import java.util.concurrent.ConcurrentHashMap
1523
import 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
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ abstract class ComposeExtension extends ComposeSettings {
4444
taskName = taskName[0].toLowerCase() + taskName.substring(1)
4545
ComposeSettings s = getOrCreateNested(taskName)
4646
s.useComposeFiles = [args[0].toString()]
47-
project.tasks.findAll { it.name.equalsIgnoreCase(taskName) }.forEach { s.isRequiredBy(it) }
47+
tasksConfigurator.setupMissingRequiredBy(taskName, s)
4848
s
4949
} else if (args.length == 1 && args[0] instanceof Closure) {
5050
ComposeSettings s = getOrCreateNested(name)

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

Lines changed: 7 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
package com.avast.gradle.dockercompose
22

3-
import com.avast.gradle.dockercompose.tasks.ComposeBuild
4-
import com.avast.gradle.dockercompose.tasks.ComposeDown
5-
import com.avast.gradle.dockercompose.tasks.ComposeDownForced
6-
import com.avast.gradle.dockercompose.tasks.ComposeLogs
7-
import com.avast.gradle.dockercompose.tasks.ComposePull
8-
import com.avast.gradle.dockercompose.tasks.ComposePush
9-
import com.avast.gradle.dockercompose.tasks.ComposeUp
10-
import com.avast.gradle.dockercompose.tasks.ServiceInfoCache
3+
114
import groovy.transform.CompileStatic
12-
import groovy.transform.PackageScope
135
import org.gradle.api.Project
146
import org.gradle.api.Task
157
import org.gradle.api.file.DirectoryProperty
@@ -21,7 +13,6 @@ import org.gradle.api.tasks.TaskProvider
2113
import org.gradle.internal.os.OperatingSystem
2214
import org.gradle.process.JavaForkOptions
2315
import org.gradle.process.ProcessForkOptions
24-
import org.gradle.util.VersionNumber
2516

2617
import javax.inject.Inject
2718
import java.nio.charset.StandardCharsets
@@ -30,17 +21,8 @@ import java.time.Duration
3021

3122
@CompileStatic
3223
abstract class ComposeSettings {
33-
final TaskProvider<ComposeUp> upTask
34-
final TaskProvider<ComposeDown> downTask
35-
final TaskProvider<ComposeDownForced> downForcedTask
36-
final TaskProvider<ComposeBuild> buildTask
37-
final TaskProvider<ComposePull> pullTask
38-
final TaskProvider<ComposeLogs> logsTask
39-
final TaskProvider<ComposePush> pushTask
40-
final Project project
24+
transient final TasksConfigurator tasksConfigurator
4125
final DockerExecutor dockerExecutor
42-
final ComposeExecutor composeExecutor
43-
final ServiceInfoCache serviceInfoCache
4426

4527
abstract ListProperty<String> getUseComposeFiles()
4628
abstract ListProperty<String> getStartedServices()
@@ -116,7 +98,6 @@ abstract class ComposeSettings {
11698

11799
@Inject
118100
ComposeSettings(Project project, String name = '', String parentName = '') {
119-
this.project = project
120101
this.nestedName = parentName + name
121102
this.safeProjectNamePrefix = generateSafeProjectNamePrefix(project)
122103

@@ -172,17 +153,9 @@ abstract class ComposeSettings {
172153

173154
this.containerLogToDir.set(project.buildDir.toPath().resolve('containers-logs').toFile())
174155

175-
upTask = project.tasks.register(name ? "${name}ComposeUp".toString() : 'composeUp', ComposeUp, { it.settings = this })
176-
buildTask = project.tasks.register(name ? "${name}ComposeBuild".toString() : 'composeBuild', ComposeBuild, { it.settings = this })
177-
pullTask = project.tasks.register(name ? "${name}ComposePull".toString() : 'composePull', ComposePull, { it.settings = this })
178-
downTask = project.tasks.register(name ? "${name}ComposeDown".toString() : 'composeDown', ComposeDown, { it.settings = this })
179-
downForcedTask = project.tasks.register(name ? "${name}ComposeDownForced".toString() : 'composeDownForced', ComposeDownForced, { it.settings = this })
180-
logsTask = project.tasks.register(name ? "${name}ComposeLogs".toString() : 'composeLogs', ComposeLogs, { it.settings = this })
181-
pushTask = project.tasks.register(name ? "${name}ComposePush".toString() : 'composePush', ComposePush, { it.settings = this })
156+
this.tasksConfigurator = new TasksConfigurator(this, project, name)
182157

183158
this.dockerExecutor = project.objects.newInstance(DockerExecutor, this)
184-
this.composeExecutor = project.objects.newInstance(ComposeExecutor, this)
185-
this.serviceInfoCache = new ServiceInfoCache(this)
186159
}
187160

188161
private static String generateSafeProjectNamePrefix(Project project) {
@@ -191,7 +164,7 @@ abstract class ComposeSettings {
191164
}
192165

193166
protected ComposeSettings cloneAsNested(String name) {
194-
def r = project.objects.newInstance(ComposeSettings, project, name, this.nestedName)
167+
def r = tasksConfigurator.newComposeSettings(name, this.nestedName)
195168

196169
r.includeDependencies.set(includeDependencies.get())
197170

@@ -239,32 +212,16 @@ abstract class ComposeSettings {
239212
r
240213
}
241214

242-
@PackageScope
243-
void isRequiredByCore(Task task, boolean fromConfigure) {
244-
task.dependsOn upTask
245-
task.finalizedBy downTask
246-
project.tasks.findAll { Task.class.isAssignableFrom(it.class) && ((Task) it).name.toLowerCase().contains('classes') }
247-
.each { classesTask ->
248-
if (fromConfigure) {
249-
upTask.get().shouldRunAfter classesTask
250-
} else {
251-
upTask.configure { it.shouldRunAfter classesTask }
252-
}
253-
}
254-
if (task instanceof ProcessForkOptions) task.doFirst { exposeAsEnvironment(task as ProcessForkOptions) }
255-
if (task instanceof JavaForkOptions) task.doFirst { exposeAsSystemProperties(task as JavaForkOptions) }
256-
}
257-
258215
void isRequiredBy(Task task) {
259-
isRequiredByCore(task, false)
216+
tasksConfigurator.isRequiredByCore(task, false)
260217
}
261218

262219
void isRequiredBy(TaskProvider<? extends Task> taskProvider) {
263-
taskProvider.configure { isRequiredByCore(it, true) }
220+
taskProvider.configure { tasksConfigurator.isRequiredByCore(it, true) }
264221
}
265222

266223
Map<String, ServiceInfo> getServicesInfos() {
267-
upTask.get().servicesInfos
224+
tasksConfigurator.getServicesInfos()
268225
}
269226

270227
void exposeAsEnvironment(ProcessForkOptions task) {
@@ -312,18 +269,6 @@ abstract class ComposeSettings {
312269
static String replaceV2Separator(String serviceName) {
313270
serviceName.replaceAll('-(\\d+)$', '_$1')
314271
}
315-
316-
boolean removeOrphans() {
317-
composeExecutor.version >= VersionNumber.parse('1.7.0') && this.removeOrphans.get()
318-
}
319-
320-
boolean scale() {
321-
def v = composeExecutor.version
322-
if (v < VersionNumber.parse('1.13.0') && this.scale) {
323-
throw new UnsupportedOperationException("docker-compose version $v doesn't support --scale option")
324-
}
325-
!this.scale.get().isEmpty()
326-
}
327272
}
328273

329274
enum RemoveImages {

0 commit comments

Comments
 (0)