From ac134051fbb20353b9dad23a1bfcc85c90e0dd4a Mon Sep 17 00:00:00 2001 From: Alexander Likhachev Date: Fri, 15 Apr 2022 19:08:12 +0300 Subject: [PATCH 1/2] Use ComposeSettings#getEnvironment only as env variables override map Reading all the environment variables at configuration time leads to marking each of them as configuration cache input, so Gradle will detect value changes and invalidate cache on any irrelevant variable value change #307 --- .../com/avast/gradle/dockercompose/ComposeExecutor.groovy | 2 +- .../com/avast/gradle/dockercompose/ComposeSettings.groovy | 1 - .../groovy/com/avast/gradle/dockercompose/DockerExecutor.groovy | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/groovy/com/avast/gradle/dockercompose/ComposeExecutor.groovy b/src/main/groovy/com/avast/gradle/dockercompose/ComposeExecutor.groovy index 723e615..54c8bda 100644 --- a/src/main/groovy/com/avast/gradle/dockercompose/ComposeExecutor.groovy +++ b/src/main/groovy/com/avast/gradle/dockercompose/ComposeExecutor.groovy @@ -46,7 +46,7 @@ class ComposeExecutor { if (settings.dockerComposeWorkingDirectory.isPresent()) { e.setWorkingDir(settings.dockerComposeWorkingDirectory.get().asFile) } - e.environment = settings.environment.get() + e.environment = System.getenv() + settings.environment.get() def finalArgs = [settings.executable.get()] finalArgs.addAll(settings.composeAdditionalArgs.get()) if (noAnsi) { diff --git a/src/main/groovy/com/avast/gradle/dockercompose/ComposeSettings.groovy b/src/main/groovy/com/avast/gradle/dockercompose/ComposeSettings.groovy index c4d0b3f..c0a60ad 100644 --- a/src/main/groovy/com/avast/gradle/dockercompose/ComposeSettings.groovy +++ b/src/main/groovy/com/avast/gradle/dockercompose/ComposeSettings.groovy @@ -168,7 +168,6 @@ abstract class ComposeSettings { executable.set('docker-compose') dockerExecutable.set('docker') } - environment.set(System.getenv()) dockerComposeStopTimeout.set(Duration.ofSeconds(10)) this.containerLogToDir.set(project.buildDir.toPath().resolve('containers-logs').toFile()) diff --git a/src/main/groovy/com/avast/gradle/dockercompose/DockerExecutor.groovy b/src/main/groovy/com/avast/gradle/dockercompose/DockerExecutor.groovy index 047b147..f01786d 100644 --- a/src/main/groovy/com/avast/gradle/dockercompose/DockerExecutor.groovy +++ b/src/main/groovy/com/avast/gradle/dockercompose/DockerExecutor.groovy @@ -25,7 +25,7 @@ class DockerExecutor { def settings = this.settings new ByteArrayOutputStream().withStream { os -> def er = exec.exec { ExecSpec e -> - e.environment = settings.environment.get() + e.environment = System.getenv() + settings.environment.get() def finalArgs = [settings.dockerExecutable.get()] finalArgs.addAll(args) e.commandLine finalArgs From 8c83164c7d3a7db35d6ea268e4e5924cb78e93f2 Mon Sep 17 00:00:00 2001 From: Alexander Likhachev Date: Fri, 15 Apr 2022 23:23:16 +0300 Subject: [PATCH 2/2] Refactor plugin to be compatible with Gradle configuration cache Fixes #307 --- .../dockercompose/ComposeExecutor.groovy | 122 ++++++++---- .../dockercompose/ComposeExtension.groovy | 2 +- .../dockercompose/ComposeSettings.groovy | 69 +------ .../{tasks => }/ServiceInfoCache.groovy | 48 +++-- .../dockercompose/TasksConfigurator.groovy | 144 ++++++++++++++ .../dockercompose/tasks/ComposeBuild.groovy | 21 ++- .../dockercompose/tasks/ComposeDown.groovy | 9 +- .../tasks/ComposeDownForced.groovy | 76 +++++--- .../dockercompose/tasks/ComposeLogs.groovy | 18 +- .../dockercompose/tasks/ComposePull.groovy | 29 ++- .../dockercompose/tasks/ComposePush.groovy | 21 ++- .../dockercompose/tasks/ComposeUp.groovy | 176 ++++++++++++------ .../dockercompose/CaptureOutputTest.groovy | 5 + .../dockercompose/ComposeExecutorTest.groovy | 2 +- .../DockerComposePluginTest.groovy | 22 ++- .../dockercompose/DockerExecutorTest.groovy | 2 + .../dockercompose/ServiceInfoCacheTest.groovy | 5 +- 17 files changed, 549 insertions(+), 222 deletions(-) rename src/main/groovy/com/avast/gradle/dockercompose/{tasks => }/ServiceInfoCache.groovy (52%) create mode 100644 src/main/groovy/com/avast/gradle/dockercompose/TasksConfigurator.groovy diff --git a/src/main/groovy/com/avast/gradle/dockercompose/ComposeExecutor.groovy b/src/main/groovy/com/avast/gradle/dockercompose/ComposeExecutor.groovy index 54c8bda..1f4b34e 100644 --- a/src/main/groovy/com/avast/gradle/dockercompose/ComposeExecutor.groovy +++ b/src/main/groovy/com/avast/gradle/dockercompose/ComposeExecutor.groovy @@ -1,10 +1,16 @@ package com.avast.gradle.dockercompose -import org.gradle.api.file.ProjectLayout +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty import org.gradle.api.internal.file.FileOperations -import org.gradle.api.invocation.Gradle import org.gradle.api.logging.Logger import org.gradle.api.logging.Logging +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters import org.gradle.internal.UncheckedException import org.gradle.process.ExecOperations import org.gradle.process.ExecSpec @@ -12,25 +18,49 @@ import org.gradle.util.VersionNumber import org.yaml.snakeyaml.Yaml import javax.inject.Inject +import java.lang.ref.WeakReference +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors -class ComposeExecutor { - private final ComposeSettings settings - private final ProjectLayout layout - private final ExecOperations exec - private final FileOperations fileOps - private final Gradle gradle +abstract class ComposeExecutor implements BuildService, AutoCloseable { + static interface Parameters extends BuildServiceParameters { + abstract DirectoryProperty getProjectDirectory() + abstract ListProperty getStartedServices() + abstract ListProperty getUseComposeFiles() + abstract Property getIncludeDependencies() + abstract DirectoryProperty getDockerComposeWorkingDirectory() + abstract MapProperty getEnvironment() + abstract Property getExecutable() + abstract Property getProjectName() + abstract ListProperty getComposeAdditionalArgs() + abstract Property getRemoveOrphans() + abstract MapProperty getScale() + } - private static final Logger logger = Logging.getLogger(ComposeExecutor.class); + static Provider getInstance(Project project, ComposeSettings settings) { + String serviceId = "${ComposeExecutor.class.canonicalName} $project.path ${settings.hashCode()}" + return project.gradle.sharedServices.registerIfAbsent(serviceId, ComposeExecutor) { + it.parameters.projectDirectory.set(project.layout.projectDirectory) + it.parameters.startedServices.set(settings.startedServices) + it.parameters.useComposeFiles.set(settings.useComposeFiles) + it.parameters.includeDependencies.set(settings.includeDependencies) + it.parameters.dockerComposeWorkingDirectory.set(settings.dockerComposeWorkingDirectory) + it.parameters.environment.set(settings.environment) + it.parameters.executable.set(settings.executable) + it.parameters.projectName.set(settings.projectName) + it.parameters.composeAdditionalArgs.set(settings.composeAdditionalArgs) + it.parameters.removeOrphans.set(settings.removeOrphans) + it.parameters.scale.set(settings.scale) + } + } @Inject - ComposeExecutor(ComposeSettings settings, ProjectLayout layout, ExecOperations exec, FileOperations fileOps, Gradle gradle) { - this.settings = settings - this.layout = layout - this.exec = exec - this.fileOps = fileOps - this.gradle = gradle - } + abstract ExecOperations getExec() + + @Inject + abstract FileOperations getFileOps() + + private static final Logger logger = Logging.getLogger(ComposeExecutor.class); void executeWithCustomOutputWithExitValue(OutputStream os, String... args) { executeWithCustomOutput(os, false, true, true, args) @@ -41,14 +71,15 @@ class ComposeExecutor { } void executeWithCustomOutput(OutputStream os, Boolean ignoreExitValue, Boolean noAnsi, Boolean captureStderr, String... args) { - def settings = this.settings def er = exec.exec { ExecSpec e -> - if (settings.dockerComposeWorkingDirectory.isPresent()) { - e.setWorkingDir(settings.dockerComposeWorkingDirectory.get().asFile) + if (parameters.dockerComposeWorkingDirectory.isPresent()) { + e.setWorkingDir(parameters.dockerComposeWorkingDirectory.get().asFile) + } else { + e.setWorkingDir(parameters.projectDirectory) } - e.environment = System.getenv() + settings.environment.get() - def finalArgs = [settings.executable.get()] - finalArgs.addAll(settings.composeAdditionalArgs.get()) + e.environment = System.getenv() + parameters.environment.get() + def finalArgs = [parameters.executable.get()] + finalArgs.addAll(parameters.composeAdditionalArgs.get()) if (noAnsi) { if (version >= VersionNumber.parse('1.28.0')) { finalArgs.addAll(['--ansi', 'never']) @@ -56,8 +87,8 @@ class ComposeExecutor { finalArgs.add('--no-ansi') } } - finalArgs.addAll(settings.useComposeFiles.get().collectMany { ['-f', it].asCollection() }) - String pn = settings.projectName + finalArgs.addAll(parameters.useComposeFiles.get().collectMany { ['-f', it].asCollection() }) + String pn = parameters.projectName.get() if (pn) { finalArgs.addAll(['-p', pn]) } @@ -73,7 +104,7 @@ class ComposeExecutor { } if (!ignoreExitValue && er.exitValue != 0) { def stdout = os != null ? os.toString().trim() : "N/A" - throw new RuntimeException("Exit-code ${er.exitValue} when calling ${settings.executable.get()}, stdout: $stdout") + throw new RuntimeException("Exit-code ${er.exitValue} when calling ${parameters.executable.get()}, stdout: $stdout") } } @@ -110,6 +141,8 @@ class ComposeExecutor { return [] } + private Set> threadsToInterruptOnClose = ConcurrentHashMap.newKeySet() + void captureContainersOutput(Closure logMethod, String... services) { // execute daemon thread that executes `docker-compose logs -f --no-color` // the -f arguments means `follow` and so this command ends when docker-compose finishes @@ -152,24 +185,34 @@ class ComposeExecutor { }) t.daemon = true t.start() - gradle.buildFinished { t.interrupt() } + threadsToInterruptOnClose.add(new WeakReference(t)) + } + + @Override + void close() throws Exception { + threadsToInterruptOnClose.forEach {threadRef -> + def thread = threadRef.get() + if (thread != null) { + thread.interrupt() + } + } } Iterable getServiceNames() { - if (!settings.startedServices.get().empty) { - if(settings.includeDependencies.get()) + if (!parameters.startedServices.get().empty) { + if(parameters.includeDependencies.get()) { - def dependentServices = getDependentServices(settings.startedServices.get()).toList() - [*settings.startedServices.get(), *dependentServices].unique() + def dependentServices = getDependentServices(parameters.startedServices.get()).toList() + [*parameters.startedServices.get(), *dependentServices].unique() } else { - settings.startedServices.get() + parameters.startedServices.get() } } else if (version >= VersionNumber.parse('1.6.0')) { execute('config', '--services').readLines() } else { - def composeFiles = settings.useComposeFiles.get().empty ? getStandardComposeFiles() : getCustomComposeFiles() + def composeFiles = parameters.useComposeFiles.get().empty ? getStandardComposeFiles() : getCustomComposeFiles() composeFiles.collectMany { composeFile -> def compose = (Map) (new Yaml().load(fileOps.file(composeFile).text)) // if there is 'version' on top-level then information about services is in 'services' sub-tree @@ -190,7 +233,7 @@ class ComposeExecutor { } Iterable getStandardComposeFiles() { - File searchDirectory = fileOps.file(settings.dockerComposeWorkingDirectory) ?: layout.projectDirectory.getAsFile() + File searchDirectory = fileOps.file(parameters.dockerComposeWorkingDirectory) ?: parameters.projectDirectory.getAsFile() def res = [] def f = findInParentDirectories('docker-compose.yml', searchDirectory) if (f != null) res.add(f) @@ -200,7 +243,7 @@ class ComposeExecutor { } Iterable getCustomComposeFiles() { - settings.useComposeFiles.get().collect { + parameters.useComposeFiles.get().collect { def f = fileOps.file(it) if (!f.exists()) { throw new IllegalArgumentException("Custom Docker Compose file not found: $f") @@ -215,4 +258,15 @@ class ComposeExecutor { f.exists() ? f : findInParentDirectories(filename, directory.parentFile) } + boolean shouldRemoveOrphans() { + version >= VersionNumber.parse('1.7.0') && parameters.removeOrphans.get() + } + + boolean isScaleSupported() { + def v = version + if (v < VersionNumber.parse('1.13.0') && parameters.scale) { + throw new UnsupportedOperationException("docker-compose version $v doesn't support --scale option") + } + !parameters.scale.get().isEmpty() + } } diff --git a/src/main/groovy/com/avast/gradle/dockercompose/ComposeExtension.groovy b/src/main/groovy/com/avast/gradle/dockercompose/ComposeExtension.groovy index cff77df..5e1eba9 100644 --- a/src/main/groovy/com/avast/gradle/dockercompose/ComposeExtension.groovy +++ b/src/main/groovy/com/avast/gradle/dockercompose/ComposeExtension.groovy @@ -44,7 +44,7 @@ abstract class ComposeExtension extends ComposeSettings { taskName = taskName[0].toLowerCase() + taskName.substring(1) ComposeSettings s = getOrCreateNested(taskName) s.useComposeFiles = [args[0].toString()] - project.tasks.findAll { it.name.equalsIgnoreCase(taskName) }.forEach { s.isRequiredBy(it) } + tasksConfigurator.setupMissingRequiredBy(taskName, s) s } else if (args.length == 1 && args[0] instanceof Closure) { ComposeSettings s = getOrCreateNested(name) diff --git a/src/main/groovy/com/avast/gradle/dockercompose/ComposeSettings.groovy b/src/main/groovy/com/avast/gradle/dockercompose/ComposeSettings.groovy index c0a60ad..c0a47db 100644 --- a/src/main/groovy/com/avast/gradle/dockercompose/ComposeSettings.groovy +++ b/src/main/groovy/com/avast/gradle/dockercompose/ComposeSettings.groovy @@ -1,15 +1,7 @@ package com.avast.gradle.dockercompose -import com.avast.gradle.dockercompose.tasks.ComposeBuild -import com.avast.gradle.dockercompose.tasks.ComposeDown -import com.avast.gradle.dockercompose.tasks.ComposeDownForced -import com.avast.gradle.dockercompose.tasks.ComposeLogs -import com.avast.gradle.dockercompose.tasks.ComposePull -import com.avast.gradle.dockercompose.tasks.ComposePush -import com.avast.gradle.dockercompose.tasks.ComposeUp -import com.avast.gradle.dockercompose.tasks.ServiceInfoCache + import groovy.transform.CompileStatic -import groovy.transform.PackageScope import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.file.DirectoryProperty @@ -21,7 +13,6 @@ import org.gradle.api.tasks.TaskProvider import org.gradle.internal.os.OperatingSystem import org.gradle.process.JavaForkOptions import org.gradle.process.ProcessForkOptions -import org.gradle.util.VersionNumber import javax.inject.Inject import java.nio.charset.StandardCharsets @@ -30,17 +21,8 @@ import java.time.Duration @CompileStatic abstract class ComposeSettings { - final TaskProvider upTask - final TaskProvider downTask - final TaskProvider downForcedTask - final TaskProvider buildTask - final TaskProvider pullTask - final TaskProvider logsTask - final TaskProvider pushTask - final Project project + transient final TasksConfigurator tasksConfigurator final DockerExecutor dockerExecutor - final ComposeExecutor composeExecutor - final ServiceInfoCache serviceInfoCache abstract ListProperty getUseComposeFiles() abstract ListProperty getStartedServices() @@ -116,7 +98,6 @@ abstract class ComposeSettings { @Inject ComposeSettings(Project project, String name = '', String parentName = '') { - this.project = project this.nestedName = parentName + name this.safeProjectNamePrefix = generateSafeProjectNamePrefix(project) @@ -172,17 +153,9 @@ abstract class ComposeSettings { this.containerLogToDir.set(project.buildDir.toPath().resolve('containers-logs').toFile()) - upTask = project.tasks.register(name ? "${name}ComposeUp".toString() : 'composeUp', ComposeUp, { it.settings = this }) - buildTask = project.tasks.register(name ? "${name}ComposeBuild".toString() : 'composeBuild', ComposeBuild, { it.settings = this }) - pullTask = project.tasks.register(name ? "${name}ComposePull".toString() : 'composePull', ComposePull, { it.settings = this }) - downTask = project.tasks.register(name ? "${name}ComposeDown".toString() : 'composeDown', ComposeDown, { it.settings = this }) - downForcedTask = project.tasks.register(name ? "${name}ComposeDownForced".toString() : 'composeDownForced', ComposeDownForced, { it.settings = this }) - logsTask = project.tasks.register(name ? "${name}ComposeLogs".toString() : 'composeLogs', ComposeLogs, { it.settings = this }) - pushTask = project.tasks.register(name ? "${name}ComposePush".toString() : 'composePush', ComposePush, { it.settings = this }) + this.tasksConfigurator = new TasksConfigurator(this, project, name) this.dockerExecutor = project.objects.newInstance(DockerExecutor, this) - this.composeExecutor = project.objects.newInstance(ComposeExecutor, this) - this.serviceInfoCache = new ServiceInfoCache(this) } private static String generateSafeProjectNamePrefix(Project project) { @@ -191,7 +164,7 @@ abstract class ComposeSettings { } protected ComposeSettings cloneAsNested(String name) { - def r = project.objects.newInstance(ComposeSettings, project, name, this.nestedName) + def r = tasksConfigurator.newComposeSettings(name, this.nestedName) r.includeDependencies.set(includeDependencies.get()) @@ -239,32 +212,16 @@ abstract class ComposeSettings { r } - @PackageScope - void isRequiredByCore(Task task, boolean fromConfigure) { - task.dependsOn upTask - task.finalizedBy downTask - project.tasks.findAll { Task.class.isAssignableFrom(it.class) && ((Task) it).name.toLowerCase().contains('classes') } - .each { classesTask -> - if (fromConfigure) { - upTask.get().shouldRunAfter classesTask - } else { - upTask.configure { it.shouldRunAfter classesTask } - } - } - if (task instanceof ProcessForkOptions) task.doFirst { exposeAsEnvironment(task as ProcessForkOptions) } - if (task instanceof JavaForkOptions) task.doFirst { exposeAsSystemProperties(task as JavaForkOptions) } - } - void isRequiredBy(Task task) { - isRequiredByCore(task, false) + tasksConfigurator.isRequiredByCore(task, false) } void isRequiredBy(TaskProvider taskProvider) { - taskProvider.configure { isRequiredByCore(it, true) } + taskProvider.configure { tasksConfigurator.isRequiredByCore(it, true) } } Map getServicesInfos() { - upTask.get().servicesInfos + tasksConfigurator.getServicesInfos() } void exposeAsEnvironment(ProcessForkOptions task) { @@ -312,18 +269,6 @@ abstract class ComposeSettings { static String replaceV2Separator(String serviceName) { serviceName.replaceAll('-(\\d+)$', '_$1') } - - boolean removeOrphans() { - composeExecutor.version >= VersionNumber.parse('1.7.0') && this.removeOrphans.get() - } - - boolean scale() { - def v = composeExecutor.version - if (v < VersionNumber.parse('1.13.0') && this.scale) { - throw new UnsupportedOperationException("docker-compose version $v doesn't support --scale option") - } - !this.scale.get().isEmpty() - } } enum RemoveImages { diff --git a/src/main/groovy/com/avast/gradle/dockercompose/tasks/ServiceInfoCache.groovy b/src/main/groovy/com/avast/gradle/dockercompose/ServiceInfoCache.groovy similarity index 52% rename from src/main/groovy/com/avast/gradle/dockercompose/tasks/ServiceInfoCache.groovy rename to src/main/groovy/com/avast/gradle/dockercompose/ServiceInfoCache.groovy index e61a1f9..e6e3dc7 100644 --- a/src/main/groovy/com/avast/gradle/dockercompose/tasks/ServiceInfoCache.groovy +++ b/src/main/groovy/com/avast/gradle/dockercompose/ServiceInfoCache.groovy @@ -1,24 +1,42 @@ -package com.avast.gradle.dockercompose.tasks +package com.avast.gradle.dockercompose + -import com.avast.gradle.dockercompose.ComposeSettings -import com.avast.gradle.dockercompose.ContainerInfo -import com.avast.gradle.dockercompose.ServiceHost -import com.avast.gradle.dockercompose.ServiceInfo import groovy.json.JsonBuilder import groovy.json.JsonSlurper +import org.gradle.api.Project +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.logging.Logger +import org.gradle.api.logging.Logging +import org.gradle.api.provider.Provider +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters import java.nio.file.Files import java.util.function.Supplier -class ServiceInfoCache { - private final ComposeSettings settings - private final File servicesInfosFile - private final File stateFile +abstract class ServiceInfoCache implements BuildService { + static interface Parameters extends BuildServiceParameters { + abstract RegularFileProperty getServicesInfosFile() + abstract RegularFileProperty getStateFile() + } - ServiceInfoCache(ComposeSettings settings) { - this.settings = settings - this.servicesInfosFile = new File(settings.project.buildDir, "dockerCompose/${settings.nestedName}servicesInfos.json") - this.stateFile = new File(settings.project.buildDir, "dockerCompose/${settings.nestedName}state.txt") + static Provider getInstance(Project project, String nestedName) { + String serviceId = "${ServiceInfoCache.class.canonicalName} $project.path $nestedName" + return project.gradle.sharedServices.registerIfAbsent(serviceId, ServiceInfoCache) { + def buildDirectory = project.layout.buildDirectory + it.parameters.servicesInfosFile = buildDirectory.file("dockerCompose/${nestedName}servicesInfos.json") + it.parameters.stateFile = buildDirectory.file("dockerCompose/${nestedName}state.txt") + } + } + + private static final Logger logger = Logging.getLogger(ServiceInfoCache.class) + + private File getServicesInfosFile() { + return parameters.servicesInfosFile.asFile.get() + } + + private File getStateFile() { + parameters.stateFile.asFile.get() } Map get(Supplier stateSupplier) { @@ -29,7 +47,7 @@ class ServiceInfoCache { if (cachedState == currentState) { return deserialized.collectEntries { k, v -> [k, deserializeServiceInfo(v)] } } else { - settings.project.logger.lifecycle("Current and cached states differs, cannot use the cached service infos.\nCached state:\n$cachedState\nCurrent state:\n$currentState") + logger.lifecycle("Current and cached states differs, cannot use the cached service infos.\nCached state:\n$cachedState\nCurrent state:\n$currentState") } } return null @@ -58,4 +76,6 @@ class ServiceInfoCache { Map udpPorts = m.udpPorts.collectEntries { k, v -> [(Integer.parseInt(k)): v] } new ContainerInfo(instanceName: m.instanceName, serviceHost: new ServiceHost(m.serviceHost as HashMap), tcpPorts: tcpPorts, udpPorts: udpPorts, inspection: m.inspection) } + + boolean startupFailed = false } diff --git a/src/main/groovy/com/avast/gradle/dockercompose/TasksConfigurator.groovy b/src/main/groovy/com/avast/gradle/dockercompose/TasksConfigurator.groovy new file mode 100644 index 0000000..e879aac --- /dev/null +++ b/src/main/groovy/com/avast/gradle/dockercompose/TasksConfigurator.groovy @@ -0,0 +1,144 @@ +package com.avast.gradle.dockercompose + +import com.avast.gradle.dockercompose.tasks.* +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider +import org.gradle.process.JavaForkOptions +import org.gradle.process.ProcessForkOptions + +@CompileStatic +class TasksConfigurator { + final ComposeSettings composeSettings + final Project project + final TaskProvider upTask + final TaskProvider downTask + final TaskProvider downForcedTask + final TaskProvider downForcedOnFailureTask + final TaskProvider buildTask + final TaskProvider pullTask + final TaskProvider logsTask + final TaskProvider pushTask + + TasksConfigurator(ComposeSettings composeSettings, Project project, String name = '') { + this.composeSettings = composeSettings + this.project = project + Provider composeExecutor = ComposeExecutor.getInstance(project, composeSettings) + Provider serviceInfoCache = ServiceInfoCache.getInstance(project, composeSettings.nestedName) + this.upTask = project.tasks.register(name ? "${name}ComposeUp".toString() : 'composeUp', ComposeUp) {task -> + task.stopContainers.set(composeSettings.stopContainers) + task.forceRecreate.set(composeSettings.forceRecreate) + task.noRecreate.set(composeSettings.noRecreate) + task.scale.set(composeSettings.scale) + task.upAdditionalArgs.set(composeSettings.upAdditionalArgs) + task.startedServices.set(composeSettings.startedServices) + task.composeLogToFile.set(composeSettings.composeLogToFile) + task.waitForTcpPorts.set(composeSettings.waitForTcpPorts) + task.retainContainersOnStartupFailure.set(composeSettings.retainContainersOnStartupFailure) + task.captureContainersOutput.set(composeSettings.captureContainersOutput) + task.captureContainersOutputToFile.set(composeSettings.captureContainersOutputToFile) + task.captureContainersOutputToFiles.set(composeSettings.captureContainersOutputToFiles) + task.waitAfterHealthyStateProbeFailure.set(composeSettings.waitAfterHealthyStateProbeFailure) + task.checkContainersRunning.set(composeSettings.checkContainersRunning) + task.waitForHealthyStateTimeout.set(composeSettings.waitForHealthyStateTimeout) + task.tcpPortsToIgnoreWhenWaiting.set(composeSettings.tcpPortsToIgnoreWhenWaiting) + task.waitForTcpPortsDisconnectionProbeTimeout.set(composeSettings.waitForTcpPortsDisconnectionProbeTimeout) + task.waitForTcpPortsTimeout.set(composeSettings.waitForTcpPortsTimeout) + task.waitAfterTcpProbeFailure.set(composeSettings.waitAfterTcpProbeFailure) + task.serviceInfoCache.set(serviceInfoCache) + task.composeExecutor.set(composeExecutor) + task.dependsOn(composeSettings.buildBeforeUp.map { buildBeforeUp -> + buildBeforeUp ? [buildTask] : [] + }) + task.dockerExecutor = composeSettings.dockerExecutor + task.finalizedBy(downForcedOnFailureTask) + } + this.buildTask = project.tasks.register(name ? "${name}ComposeBuild".toString() : 'composeBuild', ComposeBuild) {task -> + task.buildAdditionalArgs.set(composeSettings.buildAdditionalArgs) + task.startedServices.set(composeSettings.startedServices) + task.composeExecutor.set(composeExecutor) + } + this.pullTask = project.tasks.register(name ? "${name}ComposePull".toString() : 'composePull', ComposePull) {task -> + task.ignorePullFailure.set(composeSettings.ignorePullFailure) + task.pullAdditionalArgs.set(composeSettings.pullAdditionalArgs) + task.startedServices.set(composeSettings.startedServices) + task.composeExecutor.set(composeExecutor) + task.dependsOn(composeSettings.buildBeforePull.map { buildBeforePull -> + buildBeforePull ? [buildTask] : [] + }) + } + this.downTask = project.tasks.register(name ? "${name}ComposeDown".toString() : 'composeDown', ComposeDown) {task -> + configureDownForcedTask(task, composeExecutor, serviceInfoCache) + task.stopContainers.set(composeSettings.stopContainers) + } + this.downForcedTask = project.tasks.register(name ? "${name}ComposeDownForced".toString() : 'composeDownForced', ComposeDownForced) {task -> + configureDownForcedTask(task, composeExecutor, serviceInfoCache) + } + this.downForcedOnFailureTask = project.tasks.register(name ? "${name}ComposeDownForcedOnFailure".toString() : 'composeDownForcedOnFailure', ComposeDownForced) {task -> + configureDownForcedTask(task, composeExecutor, serviceInfoCache) + task.onlyIf { task.serviceInfoCache.get().startupFailed } + } + this.logsTask = project.tasks.register(name ? "${name}ComposeLogs".toString() : 'composeLogs', ComposeLogs) {task -> + task.containerLogToDir.set(composeSettings.containerLogToDir) + task.composeExecutor.set(composeExecutor) + } + this.pushTask = project.tasks.register(name ? "${name}ComposePush".toString() : 'composePush', ComposePush) {task -> + task.ignorePushFailure.set(composeSettings.ignorePushFailure) + task.pushServices.set(composeSettings.pushServices) + task.composeExecutor.set(composeExecutor) + } + } + + private void configureDownForcedTask( + ComposeDownForced task, + Provider composeExecutor, + Provider serviceInfoCache + ) { + task.dockerComposeStopTimeout.set(composeSettings.dockerComposeStopTimeout) + task.removeContainers.set(composeSettings.removeContainers) + task.startedServices.set(composeSettings.startedServices) + task.removeVolumes.set(composeSettings.removeVolumes) + task.removeImages.set(composeSettings.removeImages) + task.downAdditionalArgs.set(composeSettings.downAdditionalArgs) + task.composeLogToFile.set(composeSettings.composeLogToFile) + task.nestedName.set(composeSettings.nestedName) + task.composeExecutor.set(composeExecutor) + task.serviceInfoCache.set(serviceInfoCache) + } + + @PackageScope + void isRequiredByCore(Task task, boolean fromConfigure) { + task.dependsOn upTask + task.finalizedBy downTask + project.tasks.findAll { Task.class.isAssignableFrom(it.class) && ((Task) it).name.toLowerCase().contains('classes') } + .each { classesTask -> + if (fromConfigure) { + upTask.get().shouldRunAfter classesTask + } else { + upTask.configure { it.shouldRunAfter classesTask } + } + } + if (task instanceof ProcessForkOptions) task.doFirst { composeSettings.exposeAsEnvironment(task as ProcessForkOptions) } + if (task instanceof JavaForkOptions) task.doFirst { composeSettings.exposeAsSystemProperties(task as JavaForkOptions) } + } + + @PackageScope + Map getServicesInfos() { + upTask.get().servicesInfos + } + + @PackageScope + void setupMissingRequiredBy(String taskName, ComposeSettings settings) { + project.tasks + .findAll { Task task -> task.name.equalsIgnoreCase(taskName) } + .forEach { Task task -> settings.isRequiredBy(task) } + } + + @PackageScope + ComposeSettings newComposeSettings(String name, String nestedName) { + return project.objects.newInstance(ComposeSettings, project, name, nestedName) + } +} diff --git a/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeBuild.groovy b/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeBuild.groovy index 9fcd73c..f41a7c6 100644 --- a/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeBuild.groovy +++ b/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeBuild.groovy @@ -1,16 +1,25 @@ package com.avast.gradle.dockercompose.tasks -import com.avast.gradle.dockercompose.ComposeSettings +import com.avast.gradle.dockercompose.ComposeExecutor import groovy.transform.CompileStatic import org.gradle.api.DefaultTask +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider import org.gradle.api.tasks.Internal import org.gradle.api.tasks.TaskAction @CompileStatic -class ComposeBuild extends DefaultTask { +abstract class ComposeBuild extends DefaultTask { @Internal - ComposeSettings settings + abstract ListProperty getBuildAdditionalArgs() + + @Internal + abstract ListProperty getStartedServices() + + @Internal + abstract Property getComposeExecutor() ComposeBuild() { group = 'docker' @@ -20,8 +29,8 @@ class ComposeBuild extends DefaultTask { @TaskAction void build() { String[] args = ['build'] - args += (List)settings.buildAdditionalArgs.get() - args += (List)settings.startedServices.get() - settings.composeExecutor.execute(args) + args += (List) buildAdditionalArgs.get() + args += (List) startedServices.get() + composeExecutor.get().execute(args) } } diff --git a/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeDown.groovy b/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeDown.groovy index 47d0f2e..8947159 100644 --- a/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeDown.groovy +++ b/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeDown.groovy @@ -1,10 +1,15 @@ package com.avast.gradle.dockercompose.tasks import groovy.transform.CompileStatic +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Internal import org.gradle.api.tasks.TaskAction @CompileStatic -class ComposeDown extends ComposeDownForced { +abstract class ComposeDown extends ComposeDownForced { + @Internal + abstract Property getStopContainers() + ComposeDown() { group = 'docker' description = 'Stops and removes containers of docker-compose project (only if stopContainers is set to true)' @@ -12,7 +17,7 @@ class ComposeDown extends ComposeDownForced { @TaskAction void down() { - if (settings.stopContainers.get()) { + if (stopContainers.get()) { super.down() } else { logger.lifecycle('You\'re trying to stop the containers, but stopContainers is set to false. Please use composeDownForced task instead.') diff --git a/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeDownForced.groovy b/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeDownForced.groovy index fd13659..cb1eb0c 100644 --- a/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeDownForced.groovy +++ b/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeDownForced.groovy @@ -1,15 +1,49 @@ package com.avast.gradle.dockercompose.tasks -import com.avast.gradle.dockercompose.ComposeSettings +import com.avast.gradle.dockercompose.ComposeExecutor import com.avast.gradle.dockercompose.RemoveImages +import com.avast.gradle.dockercompose.ServiceInfoCache import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property import org.gradle.api.tasks.Internal import org.gradle.api.tasks.TaskAction import org.gradle.util.VersionNumber -class ComposeDownForced extends DefaultTask { +import java.time.Duration + +abstract class ComposeDownForced extends DefaultTask { + + @Internal + abstract Property getDockerComposeStopTimeout() + + @Internal + abstract Property getRemoveContainers() + + @Internal + abstract ListProperty getStartedServices() + + @Internal + abstract Property getRemoveVolumes() + + @Internal + abstract Property getRemoveImages() + + @Internal + abstract ListProperty getDownAdditionalArgs() + + @Internal + abstract RegularFileProperty getComposeLogToFile() + + @Internal + abstract Property getNestedName() + + @Internal + abstract Property getComposeExecutor() + @Internal - ComposeSettings settings + abstract Property getServiceInfoCache() ComposeDownForced() { group = 'docker' @@ -18,49 +52,49 @@ class ComposeDownForced extends DefaultTask { @TaskAction void down() { - def servicesToStop = settings.composeExecutor.serviceNames - settings.serviceInfoCache.clear() - settings.composeExecutor.execute(*['stop', '--timeout', settings.dockerComposeStopTimeout.get().getSeconds().toString(), *servicesToStop]) - if (settings.removeContainers.get()) { - if (settings.composeExecutor.version >= VersionNumber.parse('1.6.0')) { + def servicesToStop = composeExecutor.get().serviceNames + serviceInfoCache.get().clear() + composeExecutor.get().execute(*['stop', '--timeout', dockerComposeStopTimeout.get().getSeconds().toString(), *servicesToStop]) + if (removeContainers.get()) { + if (composeExecutor.get().version >= VersionNumber.parse('1.6.0')) { String[] args = [] - if (!settings.startedServices.get().empty) { + if (!startedServices.get().empty) { args += ['rm', '-f'] - if (settings.removeVolumes.get()) { + if (removeVolumes.get()) { args += ['-v'] } args += servicesToStop } else { args += ['down'] - switch (settings.removeImages.get()) { + switch (removeImages.get()) { case RemoveImages.All: case RemoveImages.Local: - args += ['--rmi', "${settings.removeImages.get()}".toLowerCase()] + args += ['--rmi', "${removeImages.get()}".toLowerCase()] break default: break } - if (settings.removeVolumes.get()) { + if (removeVolumes.get()) { args += ['--volumes'] } - if (settings.removeOrphans()) { + if (composeExecutor.get().shouldRemoveOrphans()) { args += '--remove-orphans' } - args += settings.downAdditionalArgs.get() + args += downAdditionalArgs.get() } def composeLog = null - if(settings.composeLogToFile.isPresent()) { - File logFile = settings.composeLogToFile.get().asFile + if (composeLogToFile.isPresent()) { + File logFile = composeLogToFile.get().asFile logger.debug "Logging docker-compose down to: $logFile" logFile.parentFile.mkdirs() composeLog = new FileOutputStream(logFile, true) } - settings.composeExecutor.executeWithCustomOutputWithExitValue(composeLog, args) + composeExecutor.get().executeWithCustomOutputWithExitValue(composeLog, args) } else { - if (!settings.startedServices.get().empty) { - settings.composeExecutor.execute(*['rm', '-f', *servicesToStop]) + if (!startedServices.get().empty) { + composeExecutor.get().execute(*['rm', '-f', *servicesToStop]) } else { - settings.composeExecutor.execute('rm', '-f') + composeExecutor.get().execute('rm', '-f') } } } diff --git a/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeLogs.groovy b/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeLogs.groovy index 80b88a5..50a18dc 100644 --- a/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeLogs.groovy +++ b/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeLogs.groovy @@ -1,16 +1,22 @@ package com.avast.gradle.dockercompose.tasks -import com.avast.gradle.dockercompose.ComposeSettings +import com.avast.gradle.dockercompose.ComposeExecutor import groovy.transform.CompileStatic import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider import org.gradle.api.tasks.Internal import org.gradle.api.tasks.TaskAction @CompileStatic -class ComposeLogs extends DefaultTask { +abstract class ComposeLogs extends DefaultTask { @Internal - ComposeSettings settings + abstract DirectoryProperty getContainerLogToDir() + + @Internal + abstract Property getComposeExecutor() ComposeLogs() { group = 'docker' @@ -19,13 +25,13 @@ class ComposeLogs extends DefaultTask { @TaskAction void logs() { - settings.composeExecutor.serviceNames.each { service -> + composeExecutor.get().serviceNames.each { service -> println "Extracting container log from service '${service}'" - File logFile = settings.containerLogToDir.get().asFile + File logFile = containerLogToDir.get().asFile logFile.mkdirs() def logStream = new FileOutputStream("${logFile.absolutePath}/${service}.log") String[] args = ['logs', '-t', service] - settings.composeExecutor.executeWithCustomOutputWithExitValue(logStream, args) + composeExecutor.get().executeWithCustomOutputWithExitValue(logStream, args) } } } diff --git a/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposePull.groovy b/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposePull.groovy index 1999046..3220d0a 100644 --- a/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposePull.groovy +++ b/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposePull.groovy @@ -1,16 +1,28 @@ package com.avast.gradle.dockercompose.tasks -import com.avast.gradle.dockercompose.ComposeSettings +import com.avast.gradle.dockercompose.ComposeExecutor import groovy.transform.CompileStatic import org.gradle.api.DefaultTask +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider import org.gradle.api.tasks.Internal import org.gradle.api.tasks.TaskAction @CompileStatic -class ComposePull extends DefaultTask { +abstract class ComposePull extends DefaultTask { @Internal - ComposeSettings settings + abstract Property getIgnorePullFailure() + + @Internal + abstract ListProperty getPullAdditionalArgs() + + @Internal + abstract ListProperty getStartedServices() + + @Internal + abstract Property getComposeExecutor() ComposePull() { group = 'docker' @@ -19,15 +31,12 @@ class ComposePull extends DefaultTask { @TaskAction void pull() { - if (settings.buildBeforePull.get()) { - settings.buildTask.get().build() - } String[] args = ['pull'] - if (settings.ignorePullFailure.get()) { + if (ignorePullFailure.get()) { args += '--ignore-pull-failures' } - args += (List)settings.pullAdditionalArgs.get() - args += (List)settings.startedServices.get() - settings.composeExecutor.execute(args) + args += (List) pullAdditionalArgs.get() + args += (List) startedServices.get() + composeExecutor.get().execute(args) } } diff --git a/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposePush.groovy b/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposePush.groovy index ab5ff23..58e22cb 100644 --- a/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposePush.groovy +++ b/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposePush.groovy @@ -1,16 +1,25 @@ package com.avast.gradle.dockercompose.tasks -import com.avast.gradle.dockercompose.ComposeSettings +import com.avast.gradle.dockercompose.ComposeExecutor import groovy.transform.CompileStatic import org.gradle.api.DefaultTask +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider import org.gradle.api.tasks.Internal import org.gradle.api.tasks.TaskAction @CompileStatic -class ComposePush extends DefaultTask { +abstract class ComposePush extends DefaultTask { @Internal - ComposeSettings settings + abstract Property getIgnorePushFailure() + + @Internal + abstract ListProperty getPushServices() + + @Internal + abstract Property getComposeExecutor() ComposePush() { group = 'docker' @@ -20,10 +29,10 @@ class ComposePush extends DefaultTask { @TaskAction void push() { String[] args = ['push'] - if (settings.ignorePushFailure.get()) { + if (ignorePushFailure.get()) { args += '--ignore-push-failures' } - args += (List)settings.pushServices.get() - settings.composeExecutor.execute(args) + args += (List) pushServices.get() + composeExecutor.get().execute(args) } } \ No newline at end of file diff --git a/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeUp.groovy b/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeUp.groovy index 08fef3d..aaefedb 100644 --- a/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeUp.groovy +++ b/src/main/groovy/com/avast/gradle/dockercompose/tasks/ComposeUp.groovy @@ -1,22 +1,93 @@ package com.avast.gradle.dockercompose.tasks -import com.avast.gradle.dockercompose.ComposeSettings +import com.avast.gradle.dockercompose.ComposeExecutor import com.avast.gradle.dockercompose.ContainerInfo +import com.avast.gradle.dockercompose.DockerExecutor import com.avast.gradle.dockercompose.ServiceHost import com.avast.gradle.dockercompose.ServiceInfo +import com.avast.gradle.dockercompose.ServiceInfoCache import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property import org.gradle.api.tasks.Internal import org.gradle.api.tasks.TaskAction +import java.time.Duration import java.time.Instant -class ComposeUp extends DefaultTask { +abstract class ComposeUp extends DefaultTask { @Internal Boolean wasReconnected = false // for tests @Internal - ComposeSettings settings + DockerExecutor dockerExecutor + + @Internal + abstract Property getStopContainers() + + @Internal + abstract Property getForceRecreate() + + @Internal + abstract Property getNoRecreate() + + @Internal + abstract MapProperty getScale() + + @Internal + abstract ListProperty getUpAdditionalArgs() + + @Internal + abstract ListProperty getStartedServices() + + @Internal + abstract RegularFileProperty getComposeLogToFile() + + @Internal + abstract Property getWaitForTcpPorts() + + @Internal + abstract Property getRetainContainersOnStartupFailure() + + @Internal + abstract Property getCaptureContainersOutput() + + @Internal + abstract RegularFileProperty getCaptureContainersOutputToFile() + + @Internal + abstract DirectoryProperty getCaptureContainersOutputToFiles() + + @Internal + abstract Property getWaitAfterHealthyStateProbeFailure() + + @Internal + abstract Property getCheckContainersRunning() + + @Internal + abstract Property getWaitForHealthyStateTimeout() + + @Internal + abstract ListProperty getTcpPortsToIgnoreWhenWaiting() + + @Internal + abstract Property getWaitForTcpPortsDisconnectionProbeTimeout() + + @Internal + abstract Property getWaitForTcpPortsTimeout() + + @Internal + abstract Property getWaitAfterTcpProbeFailure() + + @Internal + abstract Property getServiceInfoCache() + + @Internal + abstract Property getComposeExecutor() private Map servicesInfos = [:] @@ -32,8 +103,8 @@ class ComposeUp extends DefaultTask { @TaskAction void up() { - if (!settings.stopContainers.get()) { - def cachedServicesInfos = settings.serviceInfoCache.get({ getStateForCache() }) + if (!stopContainers.get()) { + def cachedServicesInfos = serviceInfoCache.get().get({ getStateForCache() }) if (cachedServicesInfos) { servicesInfos = cachedServicesInfos logger.lifecycle('Cached services infos loaded while \'stopContainers\' is set to \'false\'.') @@ -43,55 +114,52 @@ class ComposeUp extends DefaultTask { return } } - settings.serviceInfoCache.clear() + serviceInfoCache.get().clear() wasReconnected = false - if (settings.buildBeforeUp.get()) { - settings.buildTask.get().build() - } String[] args = ['up', '-d'] - if (settings.removeOrphans()) { + if (composeExecutor.get().shouldRemoveOrphans()) { args += '--remove-orphans' } - if (settings.forceRecreate.get()) { + if (forceRecreate.get()) { args += '--force-recreate' args += '--renew-anon-volumes' - } else if (settings.noRecreate.get()) { + } else if (noRecreate.get()) { args += '--no-recreate' } - if (settings.scale()) { - args += settings.scale.get().collect { service, value -> + if (composeExecutor.get().isScaleSupported()) { + args += scale.get().collect { service, value -> ['--scale', "$service=$value"] }.flatten() } - args += settings.upAdditionalArgs.get() - args += settings.startedServices.get() + args += upAdditionalArgs.get() + args += startedServices.get() try { def composeLog = null - if(settings.composeLogToFile.isPresent()) { - File logFile = settings.composeLogToFile.get().asFile + if (composeLogToFile.isPresent()) { + File logFile = composeLogToFile.get().asFile logger.debug "Logging docker-compose up to: $logFile" logFile.parentFile.mkdirs() composeLog = new FileOutputStream(logFile) } - settings.composeExecutor.executeWithCustomOutputWithExitValue(composeLog, args) - def servicesToLoad = settings.composeExecutor.getServiceNames() + composeExecutor.get().executeWithCustomOutputWithExitValue(composeLog, args) + def servicesToLoad = composeExecutor.get().getServiceNames() servicesInfos = loadServicesInfo(servicesToLoad).collectEntries { [(it.name): (it)] } startCapturing() waitForHealthyContainers(servicesInfos.values()) - if (settings.waitForTcpPorts.get()) { + if (waitForTcpPorts.get()) { servicesInfos = waitForOpenTcpPorts(servicesInfos.values()).collectEntries { [(it.name): (it)] } } printExposedPorts() - if (!settings.stopContainers.get()) { - settings.serviceInfoCache.set(servicesInfos, getStateForCache()) + if (!stopContainers.get()) { + serviceInfoCache.get().set(servicesInfos, getStateForCache()) } else { - settings.serviceInfoCache.clear() + serviceInfoCache.get().clear() } } catch (Exception e) { logger.debug("Failed to start-up Docker containers", e) - if (!settings.retainContainersOnStartupFailure.get()) { - settings.downForcedTask.get().down() + if (!retainContainersOnStartupFailure.get()) { + serviceInfoCache.get().startupFailed = true } throw e } @@ -121,44 +189,44 @@ class ComposeUp extends DefaultTask { } protected void startCapturing() { - if (settings.captureContainersOutput.get()) { - settings.composeExecutor.captureContainersOutput(logger.&lifecycle) + if (captureContainersOutput.get()) { + composeExecutor.get().captureContainersOutput(logger.&lifecycle) } - if (settings.captureContainersOutputToFile.isPresent()) { - def logFile = settings.captureContainersOutputToFile.get().asFile + if (captureContainersOutputToFile.isPresent()) { + def logFile = captureContainersOutputToFile.get().asFile logFile.parentFile.mkdirs() - settings.composeExecutor.captureContainersOutput({ logFile.append(it + '\n') }) + composeExecutor.get().captureContainersOutput({ logFile.append(it + '\n') }) } - if (settings.captureContainersOutputToFiles.isPresent()) { - def logDir = settings.captureContainersOutputToFiles.get().asFile + if (captureContainersOutputToFiles.isPresent()) { + def logDir = captureContainersOutputToFiles.get().asFile logDir.mkdirs() logDir.listFiles().each { it.delete() } servicesInfos.keySet().each { def logFile = logDir.toPath().resolve("${it}.log").toFile() - settings.composeExecutor.captureContainersOutput({ logFile.append(it + '\n') }, it) + composeExecutor.get().captureContainersOutput({ logFile.append(it + '\n') }, it) } } } @Internal protected def getStateForCache() { - settings.composeExecutor.execute('ps') + settings.composeExecutor.execute('config') + settings.startedServices.get().join(',') + composeExecutor.get().execute('ps') + composeExecutor.get().execute('config') + startedServices.get().join(',') } protected Iterable loadServicesInfo(Iterable servicesNames) { // this code is little bit complicated - the aim is to execute `docker inspect` just once (for all the containers) - Map> serviceToContainersIds = servicesNames.collectEntries { [(it) : settings.composeExecutor.getContainerIds(it)] } - Map> inspections = settings.dockerExecutor.getInspections(*serviceToContainersIds.values().flatten().unique()) + Map> serviceToContainersIds = servicesNames.collectEntries { [(it) : composeExecutor.get().getContainerIds(it)] } + Map> inspections = dockerExecutor.getInspections(*serviceToContainersIds.values().flatten().unique()) serviceToContainersIds.collect { pair -> new ServiceInfo(name: pair.key, containerInfos: pair.value.collect { createContainerInfo(inspections.get(it), pair.key) }.collectEntries { [(it.instanceName): it] } ) } } protected ContainerInfo createContainerInfo(Map inspection, String serviceName) { String containerId = inspection.Id logger.info("Container ID of service $serviceName is $containerId") - ServiceHost host = settings.dockerExecutor.getContainerHost(inspection, serviceName, logger) + ServiceHost host = dockerExecutor.getContainerHost(inspection, serviceName, logger) logger.info("Will use $host as host of service $serviceName") - def tcpPorts = settings.dockerExecutor.getTcpPortsMapping(serviceName, inspection, host) - def udpPorts = settings.dockerExecutor.getUdpPortsMapping(serviceName, inspection, host) + def tcpPorts = dockerExecutor.getTcpPortsMapping(serviceName, inspection, host) + def udpPorts = dockerExecutor.getUdpPortsMapping(serviceName, inspection, host) // docker-compose v1 uses an underscore as a separator. v2 uses a hyphen. String instanceName = inspection.Name.find(/${serviceName}_\d+$/) ?: inspection.Name.find(/${serviceName}-\d+$/) ?: @@ -177,7 +245,7 @@ class ComposeUp extends DefaultTask { serviceInfo.containerInfos.each { instanceName, containerInfo -> def firstIteration = true while (true) { - def inspection = firstIteration ? containerInfo.inspection : settings.dockerExecutor.getInspection(containerInfo.containerId) + def inspection = firstIteration ? containerInfo.inspection : dockerExecutor.getInspection(containerInfo.containerId) Map inspectionState = inspection.State String healthStatus if (inspectionState.containsKey('Health')) { @@ -187,16 +255,16 @@ class ComposeUp extends DefaultTask { break } logger.lifecycle("Waiting for ${instanceName} to become healthy (it's $healthStatus)") - if (!firstIteration) sleep(settings.waitAfterHealthyStateProbeFailure.get().toMillis()) + if (!firstIteration) sleep(waitAfterHealthyStateProbeFailure.get().toMillis()) } else { logger.debug("Service ${instanceName} or this version of Docker doesn't support healthchecks") break } - if (settings.checkContainersRunning.get() && !"running".equalsIgnoreCase(inspectionState.Status) && !"restarting".equalsIgnoreCase(inspectionState.Status)) { - throw new RuntimeException("Container ${containerInfo.containerId} of ${instanceName} is not running nor restarting. Logs:${System.lineSeparator()}${settings.dockerExecutor.getContainerLogs(containerInfo.containerId)}") + if (checkContainersRunning.get() && !"running".equalsIgnoreCase(inspectionState.Status) && !"restarting".equalsIgnoreCase(inspectionState.Status)) { + throw new RuntimeException("Container ${containerInfo.containerId} of ${instanceName} is not running nor restarting. Logs:${System.lineSeparator()}${dockerExecutor.getContainerLogs(containerInfo.containerId)}") } - if (start.plus(settings.waitForHealthyStateTimeout.get()) < Instant.now()) { - throw new RuntimeException("Container ${containerInfo.containerId} of ${instanceName} is still reported as '${healthStatus}'. Logs:${System.lineSeparator()}${settings.dockerExecutor.getContainerLogs(containerInfo.containerId)}") + if (start.plus(waitForHealthyStateTimeout.get()) < Instant.now()) { + throw new RuntimeException("Container ${containerInfo.containerId} of ${instanceName} is still reported as '${healthStatus}'. Logs:${System.lineSeparator()}${dockerExecutor.getContainerLogs(containerInfo.containerId)}") } firstIteration = false } @@ -210,14 +278,14 @@ class ComposeUp extends DefaultTask { servicesInfos.forEach { serviceInfo -> serviceInfo.containerInfos.each { instanceName, containerInfo -> containerInfo.tcpPorts - .findAll { ep, fp -> !settings.tcpPortsToIgnoreWhenWaiting.get().any { it == ep } } + .findAll { ep, fp -> !tcpPortsToIgnoreWhenWaiting.get().any { it == ep } } .forEach { exposedPort, forwardedPort -> logger.lifecycle("Probing TCP socket on ${containerInfo.host}:${forwardedPort} of '${instanceName}'") Integer portToCheck = forwardedPort while (true) { try { def s = new Socket(containerInfo.host, portToCheck) - s.setSoTimeout(settings.waitForTcpPortsDisconnectionProbeTimeout.get().toMillis() as int) + s.setSoTimeout(waitForTcpPortsDisconnectionProbeTimeout.get().toMillis() as int) try { // in case of Windows and Mac, we must ensure that the socket is not disconnected immediately // if the socket is closed then it returns -1 @@ -239,14 +307,14 @@ class ComposeUp extends DefaultTask { break } catch (Exception e) { - if (start.plus(settings.waitForTcpPortsTimeout.get()) < Instant.now()) { - throw new RuntimeException("TCP socket on ${containerInfo.host}:${portToCheck} of '${instanceName}' is still failing. Logs:${System.lineSeparator()}${settings.dockerExecutor.getContainerLogs(containerInfo.containerId)}") + if (start.plus(waitForTcpPortsTimeout.get()) < Instant.now()) { + throw new RuntimeException("TCP socket on ${containerInfo.host}:${portToCheck} of '${instanceName}' is still failing. Logs:${System.lineSeparator()}${dockerExecutor.getContainerLogs(containerInfo.containerId)}") } logger.lifecycle("Waiting for TCP socket on ${containerInfo.host}:${portToCheck} of '${instanceName}' (${e.message})") - sleep(settings.waitAfterTcpProbeFailure.get().toMillis()) - def inspection = settings.dockerExecutor.getInspection(containerInfo.containerId) - if (settings.checkContainersRunning.get() && !"running".equalsIgnoreCase(inspection.State.Status) && !"restarting".equalsIgnoreCase(inspection.State.Status)) { - throw new RuntimeException("Container ${containerInfo.containerId} of ${instanceName} is not running nor restarting. Logs:${System.lineSeparator()}${settings.dockerExecutor.getContainerLogs(containerInfo.containerId)}") + sleep(waitAfterTcpProbeFailure.get().toMillis()) + def inspection = dockerExecutor.getInspection(containerInfo.containerId) + if (checkContainersRunning.get() && !"running".equalsIgnoreCase(inspection.State.Status) && !"restarting".equalsIgnoreCase(inspection.State.Status)) { + throw new RuntimeException("Container ${containerInfo.containerId} of ${instanceName} is not running nor restarting. Logs:${System.lineSeparator()}${dockerExecutor.getContainerLogs(containerInfo.containerId)}") } ContainerInfo newContainerInfo = createContainerInfo(inspection, serviceInfo.name) Integer newForwardedPort = newContainerInfo.tcpPorts.get(exposedPort) diff --git a/src/test/groovy/com/avast/gradle/dockercompose/CaptureOutputTest.groovy b/src/test/groovy/com/avast/gradle/dockercompose/CaptureOutputTest.groovy index 910a1fc..1406b69 100644 --- a/src/test/groovy/com/avast/gradle/dockercompose/CaptureOutputTest.groovy +++ b/src/test/groovy/com/avast/gradle/dockercompose/CaptureOutputTest.groovy @@ -33,6 +33,7 @@ class CaptureOutputTest extends Specification { when: f.extension.captureContainersOutput = true + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() then: noExceptionThrown() @@ -49,6 +50,7 @@ class CaptureOutputTest extends Specification { def logFile = new File(f.project.projectDir, "web.log") when: f.extension.captureContainersOutputToFile = logFile + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() then: noExceptionThrown() @@ -65,6 +67,7 @@ class CaptureOutputTest extends Specification { def logFile = new File(f.project.projectDir, "web.log") when: f.extension.captureContainersOutputToFile = f.project.file("${logFile.absolutePath}") + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() then: noExceptionThrown() @@ -81,6 +84,7 @@ class CaptureOutputTest extends Specification { def logDir = new File(f.project.projectDir, "logDir") when: f.extension.captureContainersOutputToFiles = logDir + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() then: noExceptionThrown() @@ -98,6 +102,7 @@ class CaptureOutputTest extends Specification { def logFile = new File(f.project.projectDir, "compose.log") when: f.extension.composeLogToFile = f.project.file("${logFile.absolutePath}") + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() then: f.project.tasks.composeDown.down() diff --git a/src/test/groovy/com/avast/gradle/dockercompose/ComposeExecutorTest.groovy b/src/test/groovy/com/avast/gradle/dockercompose/ComposeExecutorTest.groovy index 3602ce0..33f0a79 100644 --- a/src/test/groovy/com/avast/gradle/dockercompose/ComposeExecutorTest.groovy +++ b/src/test/groovy/com/avast/gradle/dockercompose/ComposeExecutorTest.groovy @@ -37,7 +37,7 @@ class ComposeExecutorTest extends Specification { f.project.plugins.apply 'docker-compose' when: - def configuredServices = f.project.dockerCompose.composeExecutor.getServiceNames() + def configuredServices = ComposeExecutor.getInstance(f.project, f.project.dockerCompose).get().getServiceNames() then: configuredServices.containsAll(expectedServices) diff --git a/src/test/groovy/com/avast/gradle/dockercompose/DockerComposePluginTest.groovy b/src/test/groovy/com/avast/gradle/dockercompose/DockerComposePluginTest.groovy index 986eb88..fa0a147 100644 --- a/src/test/groovy/com/avast/gradle/dockercompose/DockerComposePluginTest.groovy +++ b/src/test/groovy/com/avast/gradle/dockercompose/DockerComposePluginTest.groovy @@ -67,7 +67,7 @@ class DockerComposePluginTest extends Specification { project.tasks.nestedComposeBuild instanceof ComposeBuild project.tasks.nestedComposeLogs instanceof ComposeLogs ComposeUp up = project.tasks.nestedComposeUp - up.settings.useComposeFiles.get() == ['test.yml'] + up.composeExecutor.get().parameters.useComposeFiles.get() == ['test.yml'] } def "is possible to access servicesInfos of nested setting"() { @@ -175,7 +175,7 @@ class DockerComposePluginTest extends Specification { project.tasks.integrationTestComposeBuild instanceof ComposeBuild project.tasks.integrationTestComposeLogs instanceof ComposeLogs ComposeUp up = project.tasks.integrationTestComposeUp - up.settings.useComposeFiles.get() == ['test.yml'] + up.composeExecutor.get().parameters.useComposeFiles.get() == ['test.yml'] task.dependsOn.find { it instanceof TaskProvider && ((TaskProvider)it).get() == project.tasks.integrationTestComposeUp } task.getFinalizedBy().getDependencies(task).any { it == project.tasks.integrationTestComposeDown } } @@ -201,6 +201,7 @@ class DockerComposePluginTest extends Specification { assert webInfo.inspection.size() > 0 } when: + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() integrationTestTask.actions.forEach { it.execute(integrationTestTask) } then: @@ -215,9 +216,11 @@ class DockerComposePluginTest extends Specification { when: f.project.dockerCompose.stopContainers = false def t = System.nanoTime() + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() def firstDuration = System.nanoTime() - t t = System.nanoTime() + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() def secondDuration = System.nanoTime() - t then: @@ -233,8 +236,10 @@ class DockerComposePluginTest extends Specification { def f = Fixture.withNginx() when: f.project.dockerCompose.stopContainers = false + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() f.project.dockerCompose.dockerExecutor.execute('kill', f.project.dockerCompose.servicesInfos.values().find().firstContainer.containerId) + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() then: noExceptionThrown() @@ -258,6 +263,7 @@ class DockerComposePluginTest extends Specification { def "exposes environment variables and system properties"() { def f = Fixture.custom(composeFileContent) f.project.plugins.apply 'java' + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() Test test = f.project.tasks.test as Test when: @@ -290,6 +296,7 @@ class DockerComposePluginTest extends Specification { def "exposes environment variables and system properties for services having dash in service name"() { def f = Fixture.custom(composeFileContent) f.project.plugins.apply 'java' + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() Test test = f.project.tasks.test as Test when: @@ -333,6 +340,7 @@ class DockerComposePluginTest extends Specification { ''') f.project.plugins.apply 'java' f.extension.projectName = 'test' + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() Test test = f.project.tasks.test as Test when: @@ -363,6 +371,7 @@ class DockerComposePluginTest extends Specification { f.extension.useComposeFiles = ['docker-compose.yml'] f.extension.environment.put 'MY_WEB_PORT', 80 f.extension.waitForTcpPorts = false // checked in assert + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() integrationTestTask.actions.forEach { it.execute(integrationTestTask) } then: @@ -377,6 +386,7 @@ class DockerComposePluginTest extends Specification { def f = Fixture.withNginx() f.extension.scale = ['web': 2] when: + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() then: thrown(UnsupportedOperationException) @@ -396,6 +406,7 @@ class DockerComposePluginTest extends Specification { assert webInfos.containsKey('web_2') || webInfos.containsKey('web-2') } when: + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() integrationTestTask.actions.forEach { it.execute(integrationTestTask) } then: @@ -410,6 +421,7 @@ class DockerComposePluginTest extends Specification { def f = Fixture.withNginx() f.project.plugins.apply 'java' f.extension.scale = ['web': 2] + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() Test test = f.project.tasks.test as Test when: @@ -448,6 +460,7 @@ class DockerComposePluginTest extends Specification { assert webInfos.size() == 0 } when: + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() integrationTestTask.actions.forEach { it.execute(integrationTestTask) } then: @@ -460,6 +473,7 @@ class DockerComposePluginTest extends Specification { def "exposes environment variables and system properties for container with custom name"() { def f = Fixture.custom(composeFileContent) f.project.plugins.apply 'java' + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() Test test = f.project.tasks.test as Test when: @@ -494,12 +508,13 @@ class DockerComposePluginTest extends Specification { f.project.dockerCompose.includeDependencies = true f.project.dockerCompose.startedServices = ['webMaster'] f.project.plugins.apply 'docker-compose' + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() Test test = f.project.tasks.test as Test when: f.project.tasks.composeDown.down() then: - def runningServices = f.project.dockerCompose.composeExecutor.execute('ps') + def runningServices = ComposeExecutor.getInstance(f.project, f.project.dockerCompose).get().execute('ps') !runningServices.contains("webMaster") !runningServices.contains("web0") !runningServices.contains("web1") @@ -534,6 +549,7 @@ class DockerComposePluginTest extends Specification { f.project.plugins.apply 'java' f.project.plugins.apply 'docker-compose' when: + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() then: f.project.dockerCompose.servicesInfos.nginx.host == f.project.dockerCompose.servicesInfos.gw.host diff --git a/src/test/groovy/com/avast/gradle/dockercompose/DockerExecutorTest.groovy b/src/test/groovy/com/avast/gradle/dockercompose/DockerExecutorTest.groovy index eff4876..facbde4 100644 --- a/src/test/groovy/com/avast/gradle/dockercompose/DockerExecutorTest.groovy +++ b/src/test/groovy/com/avast/gradle/dockercompose/DockerExecutorTest.groovy @@ -16,6 +16,7 @@ class DockerExecutorTest extends Specification { def "reads network gateway"() { def f = Fixture.withNginx() when: + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() ServiceInfo serviceInfo = f.project.tasks.composeUp.servicesInfos.find().value String networkName = serviceInfo.firstContainer.inspection.NetworkSettings.Networks.find().key @@ -30,6 +31,7 @@ class DockerExecutorTest extends Specification { def "reads container logs"() { def f = Fixture.withHelloWorld() + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() String containerId = f.extension.servicesInfos.hello.firstContainer.containerId when: diff --git a/src/test/groovy/com/avast/gradle/dockercompose/ServiceInfoCacheTest.groovy b/src/test/groovy/com/avast/gradle/dockercompose/ServiceInfoCacheTest.groovy index 5547f03..57c11b2 100644 --- a/src/test/groovy/com/avast/gradle/dockercompose/ServiceInfoCacheTest.groovy +++ b/src/test/groovy/com/avast/gradle/dockercompose/ServiceInfoCacheTest.groovy @@ -1,14 +1,15 @@ package com.avast.gradle.dockercompose -import com.avast.gradle.dockercompose.tasks.ServiceInfoCache + import spock.lang.Specification class ServiceInfoCacheTest extends Specification { def "gets what was set"() { def f = Fixture.withNginx() - def target = new ServiceInfoCache(f.project.tasks.composeUp.settings) + def target = ServiceInfoCache.getInstance(f.project, f.project.tasks.composeDown.nestedName.get()).get() when: + f.project.tasks.composeBuild.build() f.project.tasks.composeUp.up() def original = f.project.tasks.composeUp.servicesInfos target.set(original, 'state')