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

Docker compose support #22

Merged
merged 2 commits into from
Jun 29, 2021
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
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
version=1.4.0
version=2.0.0
release.useAutomaticVersion=true
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class Docker(val environment: EnvironmentExtension) {
/**
* Represents Docker stack and provides API for manipulating it.
*/
val stack by lazy { Stack(environment) }
val stack: Stack by lazy { Stack.determine(environment) }

/**
* Configures Docker stack
Expand Down
141 changes: 23 additions & 118 deletions src/main/kotlin/com/cognifide/gradle/environment/docker/Stack.kt
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
package com.cognifide.gradle.environment.docker

import com.cognifide.gradle.common.build.Behaviors
import com.cognifide.gradle.environment.EnvironmentExtension
import com.cognifide.gradle.environment.docker.runtime.Toolbox
import com.cognifide.gradle.environment.docker.stack.Compose
import com.cognifide.gradle.environment.docker.stack.Swarm

/**
* Represents project specific Docker stack and provides API for manipulating it.
*/
class Stack(val environment: EnvironmentExtension) {
abstract class Stack(val environment: EnvironmentExtension) {

private val common = environment.common
protected val common = environment.common

val internalName = common.obj.string {
convention(common.project.rootProject.name)
common.prop.string("docker.stack.name")?.let { set(it) }
}

val networkSuffix = common.obj.string {
convention("docker-net")
common.prop.string("docker.networkSuffix")?.let { set(it) }
convention("default")
common.prop.string("docker.stack.networkSuffix")?.let { set(it) }
}

val networkName = common.obj.string {
Expand All @@ -43,129 +43,34 @@ class Stack(val environment: EnvironmentExtension) {
}
}

val initTimeout = common.obj.long {
convention(30_000L)
common.prop.long("docker.stack.initTimeout")?.let { set(it) }
}

val initialized: Boolean by lazy {
var error: Exception? = null

common.progressIndicator {
message = "Initializing stack"

try {
initSwarm()
} catch (e: DockerException) {
error = e
}
}

error?.let { e ->
throw StackException("Stack cannot be initialized. Is Docker running / installed? Error '${e.message}'", e)
}

true
}

fun init() = initialized

private fun initSwarm() {
val result = DockerProcess.execQuietly {
withTimeoutMillis(initTimeout.get())
withArgs("swarm", "init")

if (environment.docker.runtime is Toolbox) {
withArgs("--advertise-addr", environment.docker.runtime.hostIp)
}
}
if (result.exitValue != 0 && !result.errorString.contains("This node is already part of a swarm")) {
throw StackException("Failed to initialize Docker Swarm. Is Docker running / installed? Error: '${result.errorString}'")
}
}

var deployRetry = common.retry { afterSecond(this@Stack.common.prop.long("docker.stack.deployRetry") ?: 30) }

fun deploy() {
init()

common.progressIndicator {
message = "Starting stack '${internalName.get()}'"

try {
val composeFilePath = environment.docker.composeFile.get().asFile.path
DockerProcess.exec {
withArgs("stack", "deploy", "-c", composeFilePath, internalName.get(), "--with-registry-auth", "--resolve-image=always")
}
} catch (e: DockerException) {
throw StackException("Failed to deploy Docker stack '${internalName.get()}'!", e)
}

message = "Awaiting started stack '${internalName.get()}'"
Behaviors.waitUntil(deployRetry.delay) { timer ->
val running = networkAvailable
if (timer.ticks == deployRetry.times && !running) {
throw StackException("Failed to start stack named '${internalName.get()}'!")
}
val running: Boolean get() = initialized && networkAvailable

!running
}
}
fun reset() {
undeploy()
deploy()
}

var undeployRetry = common.retry { afterSecond(this@Stack.common.prop.long("docker.stack.undeployRetry") ?: 30) }

fun undeploy() {
init()
abstract val initialized: Boolean

common.progressIndicator {
message = "Stopping stack '${internalName.get()}'"
abstract fun init()

try {
DockerProcess.exec { withArgs("stack", "rm", internalName.get()) }
} catch (e: DockerException) {
throw StackException("Failed to remove Docker stack '${internalName.get()}'!", e)
}
abstract fun deploy()

message = "Awaiting stopped stack '${internalName.get()}'"
Behaviors.waitUntil(undeployRetry.delay) { timer ->
val running = networkAvailable
if (timer.ticks == undeployRetry.times && running) {
throw StackException("Failed to stop stack named '${internalName.get()}'!" +
" Try to stop manually using Docker command: 'docker stack rm ${internalName.get()}'")
}
abstract fun undeploy()

running
}
}
}
abstract fun troubleshoot(): List<String>

val running: Boolean get() = initialized && networkAvailable
protected val composeFilePath get() = environment.docker.composeFile.get().asFile.path

fun reset() {
undeploy()
deploy()
}
companion object {

@Suppress("TooGenericExceptionCaught")
fun ps() = try {
DockerProcess.execString { withArgs("stack", "ps", internalName.get(), "--no-trunc") }
} catch (e: Exception) {
throw StackException("Cannot list processes in Docker stack named '${internalName.get()}'!", e)
}
fun determine(env: EnvironmentExtension) = env.prop.string("docker.stack")
?.let { of(env, it) } ?: Compose(env)

@Suppress("TooGenericExceptionCaught")
fun troubleshoot(): List<String> = mutableListOf<String>().apply {
add("Consider troubleshooting:")

try {
val out = ps()
add("* restarting Docker")
add("* using output of command: 'docker stack ps ${internalName.get()} --no-trunc':\n")
add(out)
} catch (e: Exception) {
add("* using command: 'docker stack ps ${internalName.get()} --no-trunc'")
add("* restarting Docker")
fun of(env: EnvironmentExtension, name: String): Stack = when (name.toLowerCase()) {
Compose.NAME -> Compose(env)
Swarm.NAME -> Swarm(env)
else -> throw DockerException("Unsupported Docker stack '$name'")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package com.cognifide.gradle.environment.docker.stack

import com.cognifide.gradle.common.build.Behaviors
import com.cognifide.gradle.environment.EnvironmentExtension
import com.cognifide.gradle.environment.docker.DockerException
import com.cognifide.gradle.environment.docker.DockerProcess
import com.cognifide.gradle.environment.docker.Stack
import com.cognifide.gradle.environment.docker.StackException

@Suppress("SpreadOperator")
class Compose(environment: EnvironmentExtension) : Stack(environment) {

val initTimeout = common.obj.long {
convention(30_000L)
common.prop.long("docker.compose.initTimeout")?.let { set(it) }
}

override val initialized: Boolean by lazy {
var error: Exception? = null

common.progressIndicator {
message = "Initializing stack - Docker Compose"

try {
initCompose()
} catch (e: DockerException) {
error = e
}
}

error?.let { e ->
throw StackException("Docker Compose stack cannot be initialized. Is Docker running / installed? Error '${e.message}'", e)
}

true
}

private fun initCompose() {
val result = DockerProcess.execQuietly {
withTimeoutMillis(initTimeout.get())
withArgs("compose", "version")
}
if (result.exitValue != 0) {
throw StackException("Failed to initialize Docker Compose. Is Docker running / installed? Error: '${result.errorString}'")
}
}

override fun init() {
initialized
}

var deployRetry = common.retry { afterSecond(common.prop.long("docker.compose.deployRetry") ?: 30) }

override fun deploy() {
init()

common.progressIndicator {
message = "Starting stack '${internalName.get()}'"

try {
DockerProcess.exec {
withArgs("compose", "-p", internalName.get(), "-f", composeFilePath, "up", "-d")
}
} catch (e: DockerException) {
throw StackException("Failed to deploy Docker Compose stack '${internalName.get()}'!", e)
}

message = "Awaiting started stack '${internalName.get()}'"
Behaviors.waitUntil(deployRetry.delay) { timer ->
val running = networkAvailable
if (timer.ticks == deployRetry.times && !running) {
throw StackException("Failed to start Docker Compose stack named '${internalName.get()}'!")
}

!running
}
}
}

var undeployRetry = common.retry { afterSecond(common.prop.long("docker.compose.undeployRetry") ?: 30) }

override fun undeploy() {
init()

common.progressIndicator {
message = "Stopping stack '${internalName.get()}'"

val args = arrayOf("compose", "-p", internalName.get(), "-f", composeFilePath, "down")
try {
DockerProcess.exec { withArgs(*args) }
} catch (e: DockerException) {
throw StackException("Failed to remove Docker Compose stack '${internalName.get()}'!", e)
}

message = "Awaiting stopped stack '${internalName.get()}'"
Behaviors.waitUntil(undeployRetry.delay) { timer ->
val running = networkAvailable
if (timer.ticks == undeployRetry.times && running) {
throw StackException("Failed to stop stack named '${internalName.get()}'!" +
" Try to stop manually using Docker command: 'docker ${args.joinToString(" ")}}'")
}

running
}
}
}

@Suppress("TooGenericExceptionCaught")
override fun troubleshoot(): List<String> = mutableListOf<String>().apply {
add("Consider troubleshooting:")

val psArgs = arrayOf("compose", "-p", internalName.get(), "ps", "--no-trunc")
try {
val out = try {
DockerProcess.execString { withArgs(*psArgs) }
} catch (e: Exception) {
throw StackException("Cannot list processes in Docker Compose stack named '${internalName.get()}'!", e)
}
add("* restarting Docker")
add("* using output of command: 'docker ${psArgs.joinToString(" ")}':\n")
add(out)
} catch (e: Exception) {
add("* using command: 'docker ${psArgs.joinToString(" ")}'")
add("* restarting Docker")
}
}

companion object {
const val NAME = "compose"
}
}
Loading