Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add configuration cache support #351

Merged
merged 2 commits into from
Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 88 additions & 34 deletions src/main/groovy/com/avast/gradle/dockercompose/ComposeExecutor.groovy
Original file line number Diff line number Diff line change
@@ -1,36 +1,66 @@
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
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<Parameters>, AutoCloseable {
static interface Parameters extends BuildServiceParameters {
abstract DirectoryProperty getProjectDirectory()
abstract ListProperty<String> getStartedServices()
abstract ListProperty<String> getUseComposeFiles()
abstract Property<Boolean> getIncludeDependencies()
abstract DirectoryProperty getDockerComposeWorkingDirectory()
abstract MapProperty<String, Object> getEnvironment()
abstract Property<String> getExecutable()
abstract Property<String> getProjectName()
abstract ListProperty<String> getComposeAdditionalArgs()
abstract Property<Boolean> getRemoveOrphans()
abstract MapProperty<String, Integer> getScale()
}

private static final Logger logger = Logging.getLogger(ComposeExecutor.class);
static Provider<ComposeExecutor> 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)
Expand All @@ -41,23 +71,24 @@ 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 = 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'])
} else if (version >= VersionNumber.parse('1.16.0')) {
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])
}
Expand All @@ -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")
}
}

Expand Down Expand Up @@ -110,6 +141,8 @@ class ComposeExecutor {
return []
}

private Set<WeakReference<Thread>> threadsToInterruptOnClose = ConcurrentHashMap.newKeySet()

void captureContainersOutput(Closure<Void> 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
Expand Down Expand Up @@ -152,24 +185,34 @@ class ComposeExecutor {
})
t.daemon = true
t.start()
gradle.buildFinished { t.interrupt() }
threadsToInterruptOnClose.add(new WeakReference<Thread>(t))
}

@Override
void close() throws Exception {
threadsToInterruptOnClose.forEach {threadRef ->
def thread = threadRef.get()
if (thread != null) {
thread.interrupt()
}
}
}

Iterable<String> 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<String, Object>) (new Yaml().load(fileOps.file(composeFile).text))
// if there is 'version' on top-level then information about services is in 'services' sub-tree
Expand All @@ -190,7 +233,7 @@ class ComposeExecutor {
}

Iterable<File> 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)
Expand All @@ -200,7 +243,7 @@ class ComposeExecutor {
}

Iterable<File> 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")
Expand All @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -30,17 +21,8 @@ import java.time.Duration

@CompileStatic
abstract class ComposeSettings {
final TaskProvider<ComposeUp> upTask
final TaskProvider<ComposeDown> downTask
final TaskProvider<ComposeDownForced> downForcedTask
final TaskProvider<ComposeBuild> buildTask
final TaskProvider<ComposePull> pullTask
final TaskProvider<ComposeLogs> logsTask
final TaskProvider<ComposePush> pushTask
final Project project
transient final TasksConfigurator tasksConfigurator
final DockerExecutor dockerExecutor
final ComposeExecutor composeExecutor
final ServiceInfoCache serviceInfoCache

abstract ListProperty<String> getUseComposeFiles()
abstract ListProperty<String> getStartedServices()
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -168,22 +149,13 @@ 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())

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) {
Expand All @@ -192,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())

Expand Down Expand Up @@ -240,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<? extends Task> taskProvider) {
taskProvider.configure { isRequiredByCore(it, true) }
taskProvider.configure { tasksConfigurator.isRequiredByCore(it, true) }
}

Map<String, ServiceInfo> getServicesInfos() {
upTask.get().servicesInfos
tasksConfigurator.getServicesInfos()
}

void exposeAsEnvironment(ProcessForkOptions task) {
Expand Down Expand Up @@ -313,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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading