diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..fc44fba
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.project b/.project
new file mode 100644
index 0000000..2820e02
--- /dev/null
+++ b/.project
@@ -0,0 +1,34 @@
+
+
+ provs-core
+ Project provs-core created by Buildship.
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.buildship.core.gradleprojectbuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.buildship.core.gradleprojectnature
+
+
+
+ 1628357951205
+
+ 30
+
+ org.eclipse.core.resources.regexFilterMatcher
+ node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
+
+
+
+
diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs
new file mode 100644
index 0000000..2b6d83b
--- /dev/null
+++ b/.settings/org.eclipse.buildship.core.prefs
@@ -0,0 +1,13 @@
+arguments=
+auto.sync=false
+build.scans.enabled=false
+connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
+connection.project.dir=
+eclipse.preferences.version=1
+gradle.user.home=
+java.home=/usr/lib/jvm/java-11-openjdk-amd64
+jvm.arguments=
+offline.mode=false
+override.workspace.settings=true
+show.console.view=true
+show.executions.view=true
diff --git a/bin/main/logback.xml b/bin/main/logback.xml
new file mode 100644
index 0000000..2dd0fdb
--- /dev/null
+++ b/bin/main/logback.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+ System.err
+
+ WARN
+
+
+ %d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{35}) - %msg %n
+
+
+
+
+ System.out
+
+ DEBUG
+
+
+ %d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{35}) - %msg %n
+
+
+
+
+ ./logs/provs-${byTime}.log
+
+ ./logs/provs-%d{yyyy-MM-dd}.%i.log
+ 10MB
+ 3
+ 1GB
+
+ true
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/core/Prov.kt b/bin/main/org/domaindrivenarchitecture/provs/core/Prov.kt
new file mode 100644
index 0000000..ca7d06b
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/core/Prov.kt
@@ -0,0 +1,439 @@
+package org.domaindrivenarchitecture.provs.core
+
+import org.domaindrivenarchitecture.provs.core.platforms.SHELL
+import org.domaindrivenarchitecture.provs.core.platforms.UbuntuProv
+import org.domaindrivenarchitecture.provs.core.platforms.WinProv
+import org.domaindrivenarchitecture.provs.core.processors.LocalProcessor
+import org.domaindrivenarchitecture.provs.core.processors.Processor
+import org.slf4j.LoggerFactory
+
+
+enum class ProgressType { NONE, DOTS, BASIC, FULL_LOG }
+enum class ResultMode { NONE, LAST, ALL, FAILEXIT }
+enum class OS { WINDOWS, LINUX }
+
+
+/**
+ * This main class offers methods to execute shell commands.
+ * The commands are executed locally, remotely (via ssh) or in a docker container
+ * depending on the processor which is passed to the constructor.
+ */
+open class Prov protected constructor(
+ private val processor: Processor,
+ val name: String? = null,
+ private val progressType: ProgressType = ProgressType.BASIC
+) {
+ init {
+ if (progressType == ProgressType.FULL_LOG) {
+ val log = LoggerFactory.getILoggerFactory()
+ .getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as (ch.qos.logback.classic.Logger)
+ log.level = ch.qos.logback.classic.Level.INFO
+ }
+ }
+
+ companion object Factory {
+
+ private lateinit var defaultProvInstance: Prov
+
+ fun defaultInstance(platform: String? = null): Prov {
+ return if (Factory::defaultProvInstance.isInitialized) {
+ defaultProvInstance
+ } else {
+ defaultProvInstance = newInstance(platform = platform, name = "default instance")
+ defaultProvInstance
+ }
+ }
+
+ fun newInstance(
+ processor: Processor = LocalProcessor(),
+ platform: String? = null,
+ name: String? = null,
+ progressType: ProgressType = ProgressType.BASIC
+ ): Prov {
+
+ val os = platform ?: System.getProperty("os.name")
+
+ return when {
+ os.toUpperCase().contains(OS.LINUX.name) -> UbuntuProv(processor, name, progressType)
+ os.toUpperCase().contains(OS.WINDOWS.name) -> WinProv(processor, name, progressType)
+ else -> throw Exception("OS not supported")
+ }
+ }
+ }
+
+
+ private val internalResults = arrayListOf()
+ private var level = 0
+ private var previousLevel = 0
+ private var exit = false
+ private var runInContainerWithName: String? = null
+
+
+ /**
+ * defines a task with default success behavior, i.e. returns success if all subtasks finished with success.
+ * Same as requireAll.
+ */
+ fun def(a: Prov.() -> ProvResult): ProvResult {
+ return handle(ResultMode.ALL) { a() }
+ }
+
+ /**
+ * defines a task, which returns success if the the last subtasks or last value returns success
+ */
+ fun requireLast(a: Prov.() -> ProvResult): ProvResult {
+ return handle(ResultMode.LAST) { a() }
+ }
+
+ /**
+ * defines a task, which always returns success
+ */
+ fun optional(a: Prov.() -> ProvResult): ProvResult {
+ return handle(ResultMode.NONE) { a() }
+ }
+
+ /**
+ * defines a task, which returns success if all subtasks finished with success
+ */
+ fun requireAll(a: Prov.() -> ProvResult): ProvResult {
+ return handle(ResultMode.ALL) { a() }
+ }
+
+ /**
+ * defines a task, which exits the overall execution on failure
+ */
+ fun exitOnFailure(a: Prov.() -> ProvResult): ProvResult {
+ return handle(ResultMode.FAILEXIT) { a() }
+ }
+
+ // todo: add sudo and update test
+ fun inContainer(containerName: String, a: Prov.() -> ProvResult): ProvResult {
+ runInContainerWithName = containerName
+ val res = handle(ResultMode.ALL) { a() }
+ runInContainerWithName = null
+ return res
+ }
+
+
+ /**
+ * execute program with parameters
+ */
+ fun xec(vararg s: String): ProvResult {
+ val cmd = runInContainerWithName?.let { cmdInContainer(it, *s) } ?: s
+ val result = processor.x(*cmd)
+ return ProvResult(
+ success = (result.exitCode == 0),
+ cmd = result.argsToString(),
+ out = result.out,
+ err = result.err
+ )
+ }
+
+ /**
+ * execute program with parameters without logging (to be used if secrets are involved)
+ */
+ fun xecNoLog(vararg s: String): ProvResult {
+ val cmd = runInContainerWithName?.let { cmdInContainer(it, *s) } ?: s
+ val result = processor.xNoLog(*cmd)
+ return ProvResult(
+ success = (result.exitCode == 0),
+ cmd = "***",
+ out = result.out,
+ err = "***"
+ )
+ }
+
+
+ /**
+ * Executes a command by using the shell.
+ * Be aware: Executing shell commands that incorporate unsanitized input from an untrusted source
+ * makes a program vulnerable to shell injection, a serious security flaw which can result in arbitrary command execution.
+ * Thus, the use of this method is strongly discouraged in cases where the command string is constructed from external input.
+ */
+ open fun cmd(cmd: String, dir: String? = null, sudo: Boolean = false): ProvResult {
+ throw Exception("Not implemented")
+ }
+
+
+ /**
+ * Same as method cmd but without logging of the result/output, should be used e.g. if secrets are involved.
+ * Attention: only result is NOT logged the executed command still is.
+ */
+ open fun cmdNoLog(cmd: String, dir: String? = null, sudo: Boolean = false): ProvResult {
+ throw Exception("Not implemented")
+ }
+
+
+ /**
+ * Same as method cmd but without evaluating the result for the overall success.
+ * Can be used e.g. for checks which might succeed or fail but where failure should not influence overall success
+ */
+ open fun cmdNoEval(cmd: String, dir: String? = null, sudo: Boolean = false): ProvResult {
+ throw Exception("Not implemented")
+ }
+
+
+ /**
+ * Executes command cmd and returns true in case of success else false.
+ * The success resp. failure is not evaluated, i.e. it is not taken into account for the overall success.
+ */
+ fun chk(cmd: String, dir: String? = null): Boolean {
+ return cmdNoEval(cmd, dir).success
+ }
+
+
+ /**
+ * Retrieve a secret by executing the given command.
+ * Returns the result of the command as secret.
+ */
+ fun getSecret(command: String): Secret? {
+ val result = cmdNoLog(command)
+ return if (result.success && result.out != null) {
+ addResultToEval(ProvResult(true, getCallingMethodName()))
+ Secret(result.out)
+ } else {
+ addResultToEval(ProvResult(false, getCallingMethodName(), err = result.err, exception = result.exception))
+ null
+ }
+ }
+
+
+ /**
+ * Adds a ProvResult to the overall success evaluation.
+ * Intended for use in methods which do not automatically add results.
+ */
+ fun addResultToEval(result: ProvResult) = requireAll {
+ result
+ }
+
+ /**
+ * Executes multiple shell commands. Each command must be in its own line.
+ * Multi-line commands within the script are not supported.
+ * Empty lines and comments (all text behind # in a line) are supported, i.e. they are ignored.
+ */
+ fun sh(script: String, dir: String? = null, sudo: Boolean = false) = def {
+ val lines = script.trimIndent().replace("\r\n", "\n").split("\n")
+ val linesWithoutComments = lines.stream().map { it.split("#")[0] }
+ val linesNonEmpty = linesWithoutComments.filter { it.trim().isNotEmpty() }
+
+ var success = true
+
+ for (cmd in linesNonEmpty) {
+ if (success) {
+ success = success && cmd(cmd, dir, sudo).success
+ }
+ }
+ ProvResult(success)
+ }
+
+
+ // todo: put logic in subclasses, such as UbuntuProv
+ private fun cmdInContainer(containerName: String, vararg args: String): Array {
+ return arrayOf(SHELL, "-c", "sudo docker exec $containerName " + buildCommand(*args))
+ }
+
+ private fun buildCommand(vararg args: String): String {
+ return if (args.size == 1)
+ args[0].escapeAndEncloseByDoubleQuoteForShell()
+ else
+ if (args.size == 3 && SHELL.equals(args[0]) && "-c".equals(args[1]))
+ SHELL + " -c " + args[2].escapeAndEncloseByDoubleQuoteForShell()
+ else
+ args.joinToString(separator = " ")
+ }
+
+
+ /**
+ * Provides result handling, e.g. gather results for result summary
+ */
+ private fun handle(mode: ResultMode, a: Prov.() -> ProvResult): ProvResult {
+
+ // init
+ if (level == 0) {
+ internalResults.clear()
+ previousLevel = -1
+ exit = false
+ initProgress()
+ }
+
+ // pre-handling
+ val resultIndex = internalResults.size
+ val method = getCallingMethodName()
+ val internalResult = ResultLine(level, method, null)
+ internalResults.add(internalResult)
+
+ previousLevel = level
+
+ level++
+
+ // call the actual function
+ val res = if (!exit) {
+ progress(internalResult)
+ @Suppress("UNUSED_EXPRESSION") // false positive
+ a()
+ } else {
+ ProvResult(false, out = "Exiting due to failure and mode FAILEXIT")
+ }
+
+ level--
+
+ // post-handling
+ val returnValue =
+ if (mode == ResultMode.LAST) {
+ if (internalResultIsLeaf(resultIndex) || method == "cmd")
+ res.copy() else ProvResult(res.success)
+ } else if (mode == ResultMode.ALL) {
+ // leaf
+ if (internalResultIsLeaf(resultIndex)) res.copy()
+ // evaluate subcalls' results
+ else ProvResult(cumulativeSuccessSublevel(resultIndex) ?: false)
+ } else if (mode == ResultMode.NONE) {
+ ProvResult(true)
+ } else if (mode == ResultMode.FAILEXIT) {
+ return if (res.success) {
+ ProvResult(true)
+ } else {
+ exit = true
+ ProvResult(false)
+ }
+ } else {
+ ProvResult(false, err = "mode unknown")
+ }
+
+ previousLevel = level
+
+ internalResults[resultIndex].provResult = returnValue
+
+ if (level == 0) {
+ endProgress()
+ processor.close()
+ printResults()
+ }
+
+ return returnValue
+ }
+
+
+ private fun internalResultIsLeaf(resultIndex: Int): Boolean {
+ return !(resultIndex < internalResults.size - 1 && internalResults[resultIndex + 1].level > internalResults[resultIndex].level)
+ }
+
+
+ private fun cumulativeSuccessSublevel(resultIndex: Int): Boolean? {
+ val currentLevel = internalResults[resultIndex].level
+ var res: Boolean? = null
+ var i = resultIndex + 1
+ while (i < internalResults.size && internalResults[i].level > currentLevel) {
+ if (internalResults[i].level == currentLevel + 1) {
+ res =
+ if (res == null) internalResults[i].provResult?.success else res && (internalResults[i].provResult?.success
+ ?: false)
+ }
+ i++
+ }
+ return res
+ }
+
+
+ private val ANSI_RESET = "\u001B[0m"
+ private val ANSI_BRIGHT_RED = "\u001B[91m"
+ private val ANSI_BRIGHT_GREEN = "\u001B[92m"
+ // uncomment if needed
+ // val ANSI_BLACK = "\u001B[30m"
+ // val ANSI_RED = "\u001B[31m"
+ // val ANSI_GREEN = "\u001B[32m"
+ // val ANSI_YELLOW = "\u001B[33m"
+ // val ANSI_BLUE = "\u001B[34m"
+ // val ANSI_PURPLE = "\u001B[35m"
+ // val ANSI_CYAN = "\u001B[36m"
+ // val ANSI_WHITE = "\u001B[37m"
+ val ANSI_GRAY = "\u001B[90m"
+
+ private fun printResults() {
+ println(
+ "============================================== SUMMARY " + (if (name != null) "(" + name + ") " else "") +
+ "============================================== "
+ )
+ for (result in internalResults) {
+ println(
+ result.toString().escapeNewline()
+ .replace("Success --", ANSI_BRIGHT_GREEN + "Success" + ANSI_RESET + " --")
+ .replace("FAILED --", ANSI_BRIGHT_RED + "FAILED" + ANSI_RESET + " --")
+ )
+ }
+ if (internalResults.size > 1) {
+ println("----------------------------------------------------------------------------------------------------- ")
+ println(
+ "Overall " + internalResults[0].toString().take(10)
+ .replace("Success", ANSI_BRIGHT_GREEN + "Success" + ANSI_RESET)
+ .replace("FAILED", ANSI_BRIGHT_RED + "FAILED" + ANSI_RESET)
+ )
+ }
+ println("============================================ SUMMARY END ============================================ " + newline())
+ }
+
+ private fun String.formattedAsResultLine(): String = this
+ .replace("Success", ANSI_BRIGHT_GREEN + "Success" + ANSI_RESET)
+ .replace("FAILED", ANSI_BRIGHT_RED + "FAILED" + ANSI_RESET)
+ .replace("executing...", ANSI_GRAY + "executing..." + ANSI_RESET)
+
+
+ private fun initProgress() {
+ if ((progressType == ProgressType.DOTS) || (progressType == ProgressType.BASIC)) {
+ println("---------- Processing started ----------")
+ System.out.flush()
+ }
+ }
+
+ private fun progress(line: ResultLine) {
+ if (progressType == ProgressType.DOTS) {
+ print(".")
+ System.out.flush()
+ } else if (progressType == ProgressType.BASIC) {
+ val shortLine = line.inProgress()
+ if (!shortLine.endsWith("cmd") && !shortLine.endsWith("sh")) {
+ println(shortLine.formattedAsResultLine())
+ System.out.flush()
+ }
+ }
+ }
+
+ private fun endProgress() {
+ if ((progressType == ProgressType.DOTS) || (progressType == ProgressType.BASIC)) {
+ println("---------- Processing completed ----------")
+ }
+ }
+
+}
+
+
+internal data class ResultLine(val level: Int, val method: String?, var provResult: ProvResult?) {
+ override fun toString(): String {
+ val provResult = provResult
+ return if (provResult != null) {
+ prefix(level) + (if (provResult.success) "Success -- " else "FAILED -- ") +
+ method + " " + (provResult.cmd ?: "") +
+ (if (!provResult.success && provResult.err != null) " -- Error: " + provResult.err.escapeNewline() else "")
+ } else
+ prefix(level) + method + " " + "... in progress ... "
+
+ }
+
+ fun inProgress(): String {
+ return prefix(level) + "executing... -- " + method
+ }
+
+ private fun prefix(level: Int): String {
+ return "---".repeat(level) + "> "
+ }
+}
+
+fun Prov.myfu() = def {
+ cmd("echo asdf222")
+}
+fun main() {
+
+ local().def {
+ cmd("echo asdfasdf")
+ myfu()
+ }
+}
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/core/ProvResult.kt b/bin/main/org/domaindrivenarchitecture/provs/core/ProvResult.kt
new file mode 100644
index 0000000..e625d68
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/core/ProvResult.kt
@@ -0,0 +1,26 @@
+package org.domaindrivenarchitecture.provs.core
+
+
+data class ProvResult(val success: Boolean,
+ val cmd: String? = null,
+ val out: String? = null,
+ val err: String? = null,
+ val exception: Exception? = null,
+ val exit: String? = null) {
+
+ constructor(returnCode : Int) : this(returnCode == 0)
+
+ override fun toString(): String {
+ return "ProvResult:: ${if (success) "Succeeded" else "FAILED"} -- ${if (!cmd.isNullOrEmpty()) "Name: " +
+ cmd.escapeNewline() + ", " else ""}${if (!out.isNullOrEmpty()) "Details: $out" else ""}" +
+ (exception?.run { " Exception: " + toString() } ?: "")
+ }
+
+ @Suppress("unused")
+ fun toShortString() : String {
+ return "ProvResult:: ${if (success) "Succeeded" else "FAILED"} -- " +
+ if (!success)
+ (if (out != null) "Details: $out " else "" +
+ if (err != null) " Error: " + err else "") else ""
+ }
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/core/Secret.kt b/bin/main/org/domaindrivenarchitecture/provs/core/Secret.kt
new file mode 100644
index 0000000..5772373
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/core/Secret.kt
@@ -0,0 +1,14 @@
+package org.domaindrivenarchitecture.provs.core
+
+
+open class Secret(private val value: String) {
+ override fun toString(): String {
+ return "********"
+ }
+ fun plain() : String {
+ return value
+ }
+}
+
+
+class Password(plainPassword: String) : Secret(plainPassword)
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/core/Utils.kt b/bin/main/org/domaindrivenarchitecture/provs/core/Utils.kt
new file mode 100644
index 0000000..878ea9e
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/core/Utils.kt
@@ -0,0 +1,112 @@
+package org.domaindrivenarchitecture.provs.core
+
+import org.domaindrivenarchitecture.provs.core.docker.provideContainer
+import org.domaindrivenarchitecture.provs.core.processors.ContainerStartMode
+import org.domaindrivenarchitecture.provs.core.processors.ContainerUbuntuHostProcessor
+import org.domaindrivenarchitecture.provs.core.processors.RemoteProcessor
+import org.domaindrivenarchitecture.provs.core.tags.Api
+import java.io.File
+import java.net.InetAddress
+
+/**
+ * Returns the name of the calling function but excluding some functions of the prov framework
+ * in order to return the "real" calling function.
+ * Note: names of inner functions (i.e. which are defined inside other functions) are not
+ * supported in the sense that always the name of the outer function is returned instead.
+ */
+fun getCallingMethodName(): String? {
+ val offsetVal = 1
+ val exclude = arrayOf("def", "record", "invoke", "invoke0", "handle", "def\$default", "addResultToEval")
+ // suffixes are also ignored as method names but will be added as suffix in the evaluation results
+ val suffixes = arrayOf("optional", "requireAll", "requireLast", "inContainer")
+
+ var suffix = ""
+ val callingFrame = Thread.currentThread().stackTrace
+ for (i in 0 until (callingFrame.size - 1)) {
+ if (callingFrame[i].methodName == "getCallingMethodName") {
+ var method = callingFrame[i + offsetVal].methodName
+ var inc = 0
+ while ((method in exclude) or (method in suffixes)) {
+ if (method in suffixes && suffix == "") {
+ suffix = method
+ }
+ inc++
+ method = callingFrame[i + offsetVal + inc].methodName
+ }
+ return method + if (suffix.isBlank()) "" else " ($suffix)"
+ }
+ }
+ return null
+}
+
+
+fun String.escapeNewline(): String = this.replace("\r\n", "\\n").replace("\n", "\\n")
+fun String.escapeBackslash(): String = this.replace("\\", "\\\\")
+fun String.escapeDoubleQuote(): String = this.replace("\"", "\\\"")
+fun String.escapeSingleQuote(): String = this.replace("'", "\'")
+fun String.escapeSingleQuoteForShell(): String = this.replace("'", "'\"'\"'")
+fun String.escapeProcentForPrintf(): String = this.replace("%", "%%")
+
+// see https://www.shellscript.sh/escape.html
+fun String.escapeAndEncloseByDoubleQuoteForShell(): String {
+ return "\"" + this.escapeBackslash().replace("`", "\\`").escapeDoubleQuote().replace("$", "\\$") + "\""
+}
+
+fun hostUserHome(): String = System.getProperty("user.home") + fileSeparator()
+fun newline(): String = System.getProperty("line.separator")
+fun fileSeparator(): String = File.separator
+
+
+/**
+ * Returns default local Prov instance.
+ */
+@Suppress("unused") // used by other libraries resp. KotlinScript
+fun local(): Prov {
+ return Prov.defaultInstance()
+}
+
+
+/**
+ * Returns Prov instance for remote host with remote user with provided password.
+ * If password is null, connection is done by ssh-key.
+ * Platform (Linux, Windows) must be provided if different from local platform.
+ */
+@Api // used by other libraries resp. KotlinScript
+fun remote(host: String, remoteUser: String, password: Secret? = null, platform: String? = null): Prov {
+ require(host.isNotEmpty(), { "Host must not be empty." })
+ require(remoteUser.isNotEmpty(), { "Remote user must not be empty." })
+
+ return Prov.newInstance(RemoteProcessor(InetAddress.getByName(host), remoteUser, password), platform)
+}
+
+
+/**
+ * Returns Prov instance running in a local docker container with name containerName.
+ * A potentially existing container with the same name is reused by default resp. if
+ * parameter useExistingContainer is set to true.
+ * If a new container needs to be created, on Linux systems the image _ubuntu_ is used.
+ */
+@Api // used by other libraries resp. KotlinScript
+fun docker(containerName: String = "provs_default", useExistingContainer: Boolean = true): Prov {
+
+ val os = System.getProperty("os.name")
+
+ if ("Linux".equals(os)) {
+ val defaultDockerImage = "ubuntu"
+
+ local().provideContainer(containerName, defaultDockerImage)
+
+ return Prov.newInstance(
+ ContainerUbuntuHostProcessor(
+ containerName,
+ defaultDockerImage,
+ if (useExistingContainer)
+ ContainerStartMode.USE_RUNNING_ELSE_CREATE
+ else
+ ContainerStartMode.CREATE_NEW_KILL_EXISTING
+ )
+ )
+ } else {
+ throw RuntimeException("ERROR: method docker() is currently not supported for " + os)
+ }
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/core/docker/HostDocker.kt b/bin/main/org/domaindrivenarchitecture/provs/core/docker/HostDocker.kt
new file mode 100644
index 0000000..550234a
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/core/docker/HostDocker.kt
@@ -0,0 +1,84 @@
+package org.domaindrivenarchitecture.provs.core.docker
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.core.docker.dockerimages.DockerImage
+import org.domaindrivenarchitecture.provs.core.docker.platforms.*
+import org.domaindrivenarchitecture.provs.core.platforms.UbuntuProv
+import org.domaindrivenarchitecture.provs.core.processors.ContainerStartMode
+
+/**
+ * Builds a docker image if not yet existing.
+ */
+fun Prov.dockerProvideImage(image: DockerImage, skipIfExisting: Boolean = true, sudo: Boolean = true) : ProvResult {
+ if (this is UbuntuProv) {
+ return this.dockerProvideImagePlatform(image, skipIfExisting, sudo)
+ } else {
+ throw RuntimeException("docker not yet supported for " + (this as UbuntuProv).javaClass)
+ }
+}
+
+/**
+ * Returns true if the specified docker image exists.
+ */
+fun Prov.dockerImageExists(imageName: String, sudo: Boolean = true) : Boolean {
+ if (this is UbuntuProv) {
+ return this.dockerImageExistsPlatform(imageName, sudo)
+ } else {
+ throw RuntimeException("docker not yet supported for " + (this as UbuntuProv).javaClass)
+ }
+}
+
+/**
+ * Creates and runs a new container with name _containerName_ for image _imageName_ if not yet existing.
+ * In case the container already exists, the parameter _startMode_ determines
+ * if the running container is just kept (default behavior)
+ * or if the running container is stopped and removed and a new container is created
+ * or if the method results in a failure result.
+ */
+fun Prov.provideContainer(
+ containerName: String,
+ imageName: String = "ubuntu",
+ startMode: ContainerStartMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE,
+ sudo: Boolean = true
+) : ProvResult {
+ if (this is UbuntuProv) {
+ return this.provideContainerPlatform(containerName, imageName, startMode, sudo)
+ } else {
+ throw RuntimeException("docker not yet supported for " + (this as UbuntuProv).javaClass)
+ }
+}
+
+
+fun Prov.containerRuns(containerName: String, sudo: Boolean = true) : Boolean {
+ if (this is UbuntuProv) {
+ return this.containerRunsPlatform(containerName, sudo)
+ } else {
+ throw RuntimeException("docker not yet supported for " + (this as UbuntuProv).javaClass)
+ }
+}
+
+
+fun Prov.runContainer(
+ containerName: String = "provs_default",
+ imageName: String = "ubuntu",
+ sudo: Boolean = true
+) : ProvResult {
+ if (this is UbuntuProv) {
+ return this.runContainerPlatform(containerName, imageName, sudo)
+ } else {
+ throw RuntimeException("docker not yet supported for " + (this as UbuntuProv).javaClass)
+ }
+}
+
+
+fun Prov.exitAndRmContainer(
+ containerName: String,
+ sudo: Boolean = true
+) : ProvResult {
+ if (this is UbuntuProv) {
+ return this.exitAndRmContainerPlatform(containerName, sudo)
+ } else {
+ throw RuntimeException("docker not yet supported for " + (this as UbuntuProv).javaClass)
+ }
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/core/docker/dockerimages/DockerImages.kt b/bin/main/org/domaindrivenarchitecture/provs/core/docker/dockerimages/DockerImages.kt
new file mode 100644
index 0000000..1efbc40
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/core/docker/dockerimages/DockerImages.kt
@@ -0,0 +1,33 @@
+package org.domaindrivenarchitecture.provs.core.docker.dockerimages
+
+
+interface DockerImage {
+ fun imageName() : String
+ fun imageText() : String
+}
+
+/**
+ * Provides a docker image based on ubuntu additionally with a non-root default user and sudo isntalled
+ */
+class UbuntuPlusUser(private val userName: String = "testuser") : DockerImage {
+
+ override fun imageName(): String {
+ return "ubuntu_plus_user"
+ }
+
+ override fun imageText(): String {
+ return """
+FROM ubuntu:18.04
+
+ARG DEBIAN_FRONTEND=noninteractive
+
+RUN apt-get update && apt-get -y install sudo
+RUN useradd -m $userName && echo "$userName:$userName" | chpasswd && adduser $userName sudo
+RUN echo "$userName ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/$userName
+
+USER $userName
+CMD /bin/bash
+WORKDIR /home/$userName
+"""
+ }
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/core/docker/platforms/UbuntuHostDocker.kt b/bin/main/org/domaindrivenarchitecture/provs/core/docker/platforms/UbuntuHostDocker.kt
new file mode 100644
index 0000000..db33835
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/core/docker/platforms/UbuntuHostDocker.kt
@@ -0,0 +1,101 @@
+package org.domaindrivenarchitecture.provs.core.docker.platforms
+
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.core.docker.containerRuns
+import org.domaindrivenarchitecture.provs.core.docker.dockerImageExists
+import org.domaindrivenarchitecture.provs.core.docker.exitAndRmContainer
+import org.domaindrivenarchitecture.provs.core.docker.dockerimages.DockerImage
+import org.domaindrivenarchitecture.provs.core.escapeSingleQuote
+import org.domaindrivenarchitecture.provs.core.fileSeparator
+import org.domaindrivenarchitecture.provs.core.hostUserHome
+import org.domaindrivenarchitecture.provs.core.platforms.UbuntuProv
+import org.domaindrivenarchitecture.provs.core.processors.ContainerStartMode
+
+
+fun UbuntuProv.provideContainerPlatform(
+ containerName: String,
+ imageName: String = "ubuntu",
+ startMode: ContainerStartMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE,
+ sudo: Boolean = true
+): ProvResult = requireLast {
+ val dockerCmd = if (sudo) "sudo docker " else "docker "
+
+ if (startMode == ContainerStartMode.CREATE_NEW_KILL_EXISTING) {
+ exitAndRmContainer(containerName)
+ }
+ if ((startMode == ContainerStartMode.CREATE_NEW_KILL_EXISTING) || (startMode == ContainerStartMode.CREATE_NEW_FAIL_IF_EXISTING)) {
+ if (!cmd(dockerCmd + "run -dit --name=$containerName $imageName").success) {
+ throw RuntimeException("could not start docker")
+ }
+ } else if (startMode == ContainerStartMode.USE_RUNNING_ELSE_CREATE) {
+ val runCheckResult = cmdNoEval(dockerCmd + "inspect -f '{{.State.Running}}' $containerName")
+
+ // if either container not found or container found but not running
+ if (!runCheckResult.success || "false\n" == runCheckResult.out) {
+ cmdNoEval(dockerCmd + "rm -f $containerName")
+ cmd(dockerCmd + "run -dit --name=$containerName $imageName")
+ }
+ }
+ ProvResult(containerRuns(containerName, sudo))
+}
+
+
+fun UbuntuProv.containerRunsPlatform(containerName: String, sudo: Boolean = true): Boolean {
+ val dockerCmd = if (sudo) "sudo docker " else "docker "
+ return cmdNoEval(dockerCmd + "inspect -f '{{.State.Running}}' $containerName").out?.equals("true\n") ?: false
+}
+
+
+fun UbuntuProv.runContainerPlatform(
+ containerName: String = "defaultProvContainer",
+ imageName: String = "ubuntu",
+ sudo: Boolean = true
+) = def {
+ val dockerCmd = if (sudo) "sudo docker " else "docker "
+ cmd(dockerCmd + "run -dit --name=$containerName $imageName")
+}
+
+
+fun UbuntuProv.containerExecPlatform(containerName: String, cmd: String, sudo: Boolean = true) = def {
+ val dockerCmd = if (sudo) "sudo docker " else "docker "
+ cmd(dockerCmd + "exec $containerName $cmd")
+}
+
+
+fun UbuntuProv.dockerProvideImagePlatform(image: DockerImage, skipIfExisting: Boolean, sudo: Boolean): ProvResult {
+ val dockerCmd = if (sudo) "sudo docker " else "docker "
+
+ if (skipIfExisting && dockerImageExists(image.imageName())) {
+ return ProvResult(true)
+ }
+
+ val path = hostUserHome() + "tmp_docker_img" + fileSeparator()
+
+ if (!xec("test", "-d", path).success) {
+ cmd("cd ${hostUserHome()} && mkdir tmp_docker_img")
+ }
+
+ cmd("cd $path && printf '${image.imageText().escapeSingleQuote()}' > Dockerfile")
+
+ return cmd("cd $path && "+dockerCmd+"build --tag ${image.imageName()} .")
+}
+
+
+fun UbuntuProv.dockerImageExistsPlatform(imageName: String, sudo: Boolean): Boolean {
+ val dockerCmd = if (sudo) "sudo docker " else "docker "
+
+ return (cmdNoEval(dockerCmd + "images $imageName -q").out != "")
+}
+
+
+fun UbuntuProv.exitAndRmContainerPlatform(
+ containerName: String,
+ sudo: Boolean
+) = requireAll {
+ val dockerCmd = if (sudo) "sudo docker " else "docker "
+
+ if (containerRuns(containerName)) {
+ cmd(dockerCmd + "stop $containerName")
+ }
+ cmd(dockerCmd + "rm $containerName")
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/core/entry/Entry.kt b/bin/main/org/domaindrivenarchitecture/provs/core/entry/Entry.kt
new file mode 100644
index 0000000..c329021
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/core/entry/Entry.kt
@@ -0,0 +1,37 @@
+package org.domaindrivenarchitecture.provs.core.entry
+
+/**
+ * Calls a static method of a class.
+ * Only methods are supported with either no parameters or with one vararg parameter of type String.
+ * Methods with a vararg parameter must be called with at least one argument.
+ *
+ * @param args specify class and (optionally) method and parameters, in detail:
+ * @param args[0] fully-qualified class name of the class to be called
+ * @param args[1] (optional) static method of the class with a vararg parameter of type String; if not specified, the "main" method is used
+ * @param args[2...] (optional) String parameters that are passed to the method; can be only used if method name (args[1]) is provided
+ */
+fun main(vararg args: String) {
+
+ if (args.isNotEmpty()) {
+ val className = args[0]
+
+ val jClass = Class.forName(className)
+
+ val parameterTypeStringArray = arrayOf>(
+ Array::class.java
+ )
+ val method = if (args.size == 1) {
+ jClass.getMethod("main", *parameterTypeStringArray)
+ } else {
+ jClass.getMethod(args[1], *parameterTypeStringArray)
+ }
+
+ if (args.size <= 2) {
+ method.invoke(null, emptyArray())
+ } else {
+ method.invoke(null, args.drop(2).toTypedArray())
+ }
+ } else {
+ println("Usage: ")
+ }
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/core/platforms/UbuntuProv.kt b/bin/main/org/domaindrivenarchitecture/provs/core/platforms/UbuntuProv.kt
new file mode 100644
index 0000000..67fae5b
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/core/platforms/UbuntuProv.kt
@@ -0,0 +1,36 @@
+package org.domaindrivenarchitecture.provs.core.platforms
+
+import org.domaindrivenarchitecture.provs.core.ProgressType
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.core.escapeAndEncloseByDoubleQuoteForShell
+import org.domaindrivenarchitecture.provs.core.processors.LocalProcessor
+import org.domaindrivenarchitecture.provs.core.processors.Processor
+
+const val SHELL = "/bin/bash"
+
+
+class UbuntuProv internal constructor(processor : Processor = LocalProcessor(), name: String? = null, progressType: ProgressType)
+ : Prov(processor, name, progressType) {
+
+ override fun cmd(cmd: String, dir: String?, sudo: Boolean) : ProvResult = def {
+ xec(SHELL, "-c", commandWithDirAndSudo(cmd, dir, sudo))
+ }
+
+ override fun cmdNoLog(cmd: String, dir: String?, sudo: Boolean) : ProvResult {
+ return xecNoLog(SHELL, "-c", commandWithDirAndSudo(cmd, dir, sudo))
+ }
+
+ override fun cmdNoEval(cmd: String, dir: String?, sudo: Boolean) : ProvResult {
+ return xec(SHELL, "-c", commandWithDirAndSudo(cmd, dir, sudo))
+ }
+}
+
+private fun commandWithDirAndSudo(cmd: String, dir: String?, sudo: Boolean): String {
+ val cmdWithDir= if (dir == null) cmd else "cd $dir && $cmd"
+ return if (sudo) cmdWithDir.sudoize() else cmdWithDir
+}
+
+private fun String.sudoize(): String {
+ return "sudo " + SHELL + " -c " + this.escapeAndEncloseByDoubleQuoteForShell()
+}
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/core/platforms/WinProv.kt b/bin/main/org/domaindrivenarchitecture/provs/core/platforms/WinProv.kt
new file mode 100644
index 0000000..66b787e
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/core/platforms/WinProv.kt
@@ -0,0 +1,30 @@
+package org.domaindrivenarchitecture.provs.core.platforms
+
+import org.domaindrivenarchitecture.provs.core.ProgressType
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.core.processors.LocalProcessor
+import org.domaindrivenarchitecture.provs.core.processors.Processor
+
+
+class WinProv internal constructor(processor : Processor = LocalProcessor(), name: String? = null, progressType: ProgressType)
+ : Prov(processor, name, progressType) {
+
+ private val SHELL = "cmd.exe"
+
+ override fun cmd(cmd: String, dir: String?, sudo: Boolean) : ProvResult = def {
+ require(!sudo, {"sudo not supported"})
+ xec(SHELL, "/c", if (dir == null) cmd else "cd $dir && $cmd")
+ }
+
+ override fun cmdNoLog(cmd: String, dir: String?, sudo: Boolean) : ProvResult = def {
+ require(!sudo, {"sudo not supported"})
+ xecNoLog(SHELL, "/c", if (dir == null) cmd else "cd $dir && $cmd")
+ }
+
+
+ override fun cmdNoEval(cmd: String, dir: String?, sudo: Boolean) : ProvResult {
+ require(!sudo, {"sudo not supported"})
+ return xec(SHELL, "/c", if (dir == null) cmd else "cd $dir && $cmd")
+ }
+}
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/core/processors/ContainerUbuntuHostProcessor.kt b/bin/main/org/domaindrivenarchitecture/provs/core/processors/ContainerUbuntuHostProcessor.kt
new file mode 100644
index 0000000..d1e219c
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/core/processors/ContainerUbuntuHostProcessor.kt
@@ -0,0 +1,69 @@
+package org.domaindrivenarchitecture.provs.core.processors
+
+import org.domaindrivenarchitecture.provs.core.ProgressType
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.docker.provideContainer
+import org.domaindrivenarchitecture.provs.core.escapeAndEncloseByDoubleQuoteForShell
+import org.domaindrivenarchitecture.provs.core.platforms.SHELL
+import org.domaindrivenarchitecture.provs.core.tags.Api
+
+enum class ContainerStartMode {
+ USE_RUNNING_ELSE_CREATE,
+ CREATE_NEW_KILL_EXISTING,
+ CREATE_NEW_FAIL_IF_EXISTING
+}
+
+enum class ContainerEndMode {
+ EXIT_AND_REMOVE,
+ KEEP_RUNNING
+}
+
+open class ContainerUbuntuHostProcessor(
+ private val containerName: String = "default_provs_container",
+ @Api // suppress false positive warning
+ private val dockerImage: String = "ubuntu",
+ @Api // suppress false positive warning
+ private val startMode: ContainerStartMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE,
+ private val endMode: ContainerEndMode = ContainerEndMode.KEEP_RUNNING,
+ @Api // suppress false positive warning
+ private val sudo: Boolean = true
+) : Processor {
+ private val dockerCmd = if (sudo) "sudo docker " else "docker "
+ private var localExecution = LocalProcessor()
+ private var a = Prov.newInstance(name = "LocalProcessor for Docker operations", progressType = ProgressType.NONE)
+
+ init {
+ val r = a.provideContainer(containerName, dockerImage, startMode, sudo)
+ if (!r.success)
+ throw RuntimeException("Could not start docker image: " + r.toString(), r.exception)
+ }
+
+ override fun x(vararg args: String): ProcessResult {
+ return localExecution.x("sh", "-c", dockerCmd + "exec $containerName " + buildCommand(*args))
+ }
+
+ override fun xNoLog(vararg args: String): ProcessResult {
+ return localExecution.xNoLog("sh", "-c", dockerCmd + "exec $containerName " + buildCommand(*args))
+ }
+
+ private fun exitAndRm() {
+ localExecution.x(SHELL, "-c", dockerCmd + "stop $containerName")
+ localExecution.x(SHELL, "-c", dockerCmd + "rm $containerName")
+ }
+
+ private fun quoteString(s: String): String {
+ return s.escapeAndEncloseByDoubleQuoteForShell()
+ }
+
+ private fun buildCommand(vararg args: String) : String {
+ return if (args.size == 1) quoteString(args[0]) else
+ if (args.size == 3 && SHELL == args[0] && "-c" == args[1]) SHELL + " -c " + quoteString(args[2])
+ else args.joinToString(separator = " ")
+ }
+
+ protected fun finalize() {
+ if (endMode == ContainerEndMode.EXIT_AND_REMOVE) {
+ exitAndRm()
+ }
+ }
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/core/processors/LocalProcessor.kt b/bin/main/org/domaindrivenarchitecture/provs/core/processors/LocalProcessor.kt
new file mode 100644
index 0000000..06e245d
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/core/processors/LocalProcessor.kt
@@ -0,0 +1,77 @@
+package org.domaindrivenarchitecture.provs.core.processors
+
+import org.domaindrivenarchitecture.provs.core.escapeNewline
+import org.slf4j.LoggerFactory
+import java.io.File
+import java.io.IOException
+import java.nio.charset.Charset
+
+
+private fun getOsName(): String {
+ return System.getProperty("os.name")
+}
+
+open class LocalProcessor : Processor {
+
+ companion object {
+ @Suppress("JAVA_CLASS_ON_COMPANION")
+ private val log = LoggerFactory.getLogger(javaClass.enclosingClass)
+
+ var charset: Charset = if (getOsName().contains("Windows")) Charset.forName("Windows-1252") else Charset.defaultCharset()
+ init {
+ log.info("os.name: " + getOsName())
+ log.info("user.home: " + System.getProperty("user.home"))
+ }
+ }
+
+ private fun workingDir() : String
+ {
+ return System.getProperty("user.home") ?: File.separator
+ }
+
+ override fun x(vararg args: String): ProcessResult {
+ return execute(true, *args)
+ }
+
+
+ override fun xNoLog(vararg args: String): ProcessResult {
+ return execute(false, *args)
+ }
+
+ private fun execute(logging: Boolean, vararg args: String): ProcessResult {
+ try {
+ var prefix = "******************** Prov: "
+ if (logging) {
+ for (arg in args) {
+ prefix += " \"${arg.escapeNewline()}\""
+ }
+ } else {
+ prefix += "\"xxxxxxxx\""
+ }
+ log.info(prefix)
+
+ val proc = ProcessBuilder(args.toList())
+ .directory(File(workingDir()))
+ .redirectOutput(ProcessBuilder.Redirect.PIPE)
+ .redirectError(ProcessBuilder.Redirect.PIPE)
+ .start()
+
+ val c = proc.waitFor()
+
+ val r = ProcessResult(
+ c,
+ proc.inputStream.bufferedReader(charset).readText(),
+ proc.errorStream.bufferedReader(charset).readText(),
+ args = args
+ )
+ if (logging) {
+ log.info(r.toString())
+ }
+ return r
+
+ } catch (e: IOException) {
+ e.printStackTrace()
+ return ProcessResult(-1, ex = e)
+ }
+ }
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/core/processors/PrintOnlyProcessor.kt b/bin/main/org/domaindrivenarchitecture/provs/core/processors/PrintOnlyProcessor.kt
new file mode 100644
index 0000000..868d0ef
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/core/processors/PrintOnlyProcessor.kt
@@ -0,0 +1,20 @@
+package org.domaindrivenarchitecture.provs.core.processors
+
+
+@Suppress("unused") // used externally
+class PrintOnlyProcessor : Processor {
+
+ override fun x(vararg args: String): ProcessResult
+ {
+ print("PrintOnlyProcessor >>> ")
+ for (n in args) print("\"$n\" ")
+ println()
+ return ProcessResult(0, args = args)
+ }
+
+ override fun xNoLog(vararg args: String): ProcessResult
+ {
+ print("PrintOnlyProcessor >>> ********")
+ return ProcessResult(0, args = args)
+ }
+}
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/core/processors/Processor.kt b/bin/main/org/domaindrivenarchitecture/provs/core/processors/Processor.kt
new file mode 100644
index 0000000..c868066
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/core/processors/Processor.kt
@@ -0,0 +1,56 @@
+package org.domaindrivenarchitecture.provs.core.processors
+
+
+interface Processor {
+ fun x(vararg args: String): ProcessResult
+ fun xNoLog(vararg args: String): ProcessResult
+ fun close() {
+ // no action needed for most processors; if action is needed when closing, this method must be overwritten in the subclass
+ }
+}
+
+
+data class ProcessResult(val exitCode: Int, val out: String? = null, val err: String? = null, val ex: Exception? = null, val args: Array = emptyArray()) {
+
+ private fun success(): Boolean {
+ return (exitCode == 0)
+ }
+
+ fun argsToString() : String {
+ return args.joinToString(
+ separator = ", ",
+ prefix = "[",
+ postfix = "]",
+ limit = 4,
+ truncated = " ..."
+ )
+ }
+
+ override fun toString(): String {
+ return "--->>> ProcessResult: ${if (success()) "Succeeded" else "FAILED"} -- Code: $exitCode, ${if (!out.isNullOrEmpty()) "Out: $out, " else ""}${if (!err.isNullOrEmpty()) "Err: $err" else ""}" + argsToString()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ProcessResult
+
+ if (exitCode != other.exitCode) return false
+ if (out != other.out) return false
+ if (err != other.err) return false
+ if (ex != other.ex) return false
+ if (!args.contentEquals(other.args)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = exitCode
+ result = 31 * result + (out?.hashCode() ?: 0)
+ result = 31 * result + (err?.hashCode() ?: 0)
+ result = 31 * result + ex.hashCode()
+ result = 31 * result + args.contentHashCode()
+ return result
+ }
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/core/processors/RemoteUbuntuProcessor.kt b/bin/main/org/domaindrivenarchitecture/provs/core/processors/RemoteUbuntuProcessor.kt
new file mode 100644
index 0000000..eae5792
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/core/processors/RemoteUbuntuProcessor.kt
@@ -0,0 +1,127 @@
+package org.domaindrivenarchitecture.provs.core.processors
+
+import org.domaindrivenarchitecture.provs.core.Secret
+import org.domaindrivenarchitecture.provs.core.escapeAndEncloseByDoubleQuoteForShell
+import org.domaindrivenarchitecture.provs.core.escapeNewline
+import org.domaindrivenarchitecture.provs.core.platforms.SHELL
+import net.schmizz.sshj.SSHClient
+import net.schmizz.sshj.connection.channel.direct.Session
+import net.schmizz.sshj.connection.channel.direct.Session.Command
+import net.schmizz.sshj.transport.verification.PromiscuousVerifier
+import org.slf4j.LoggerFactory
+import java.io.BufferedReader
+import java.io.File
+import java.io.IOException
+import java.io.InputStreamReader
+import java.net.InetAddress
+import java.util.concurrent.TimeUnit
+
+
+class RemoteProcessor(ip: InetAddress, user: String, password: Secret? = null) : Processor {
+
+ companion object {
+ @Suppress("JAVA_CLASS_ON_COMPANION")
+ private val log = LoggerFactory.getLogger(javaClass.enclosingClass)
+ }
+
+ private val ssh = SSHClient()
+
+ init {
+ try {
+ log.info("Connecting to $ip with user: $user with " + if (password != null) "password" else "ssh-key")
+
+ ssh.loadKnownHosts()
+
+ // todo: replace PromiscuousVerifier by more secure solution
+ ssh.addHostKeyVerifier(PromiscuousVerifier())
+ ssh.connect(ip)
+
+ if (password != null) {
+ ssh.authPassword(user, password.plain())
+ } else {
+ val base = System.getProperty("user.home") + File.separator + ".ssh" + File.separator
+ ssh.authPublickey(user, base + "id_rsa", base + "id_dsa", base + "id_ed25519", base + "id_ecdsa")
+ }
+ } catch (e: Exception) {
+ try {
+ ssh.disconnect()
+ } finally {
+ log.error("Got exception when initializing ssh: " + e.message)
+ throw RuntimeException("Error when initializing ssh", e)
+ }
+ }
+ }
+
+ override fun x(vararg args: String): ProcessResult {
+ return execute(true, *args)
+ }
+
+ override fun xNoLog(vararg args: String): ProcessResult {
+ return execute(false, *args)
+ }
+
+ private fun execute(logging: Boolean, vararg args: String): ProcessResult {
+ var prefix = "******************** Prov: "
+ if (logging) {
+ for (arg in args) {
+ prefix += " \"${arg.escapeNewline()}\""
+ }
+ } else {
+ prefix += "\"xxxxxxxx\""
+ }
+ log.info(prefix)
+
+ val cmdString: String =
+ if (args.size == 1)
+ args[0].escapeAndEncloseByDoubleQuoteForShell()
+ else
+ if (args.size == 3 && SHELL == args[0] && "-c" == args[1])
+ SHELL + " -c " + args[2].escapeAndEncloseByDoubleQuoteForShell()
+ else
+ args.joinToString(separator = " ")
+
+ var session: Session? = null
+
+ try {
+ session = ssh.startSession()
+
+ val cmd: Command = session!!.exec(cmdString)
+ val out = BufferedReader(InputStreamReader(cmd.inputStream)).use { it.readText() }
+ val err = BufferedReader(InputStreamReader(cmd.errorStream)).use { it.readText() }
+ cmd.join(100, TimeUnit.SECONDS)
+
+ val cmdRes = ProcessResult(cmd.exitStatus, out, err, args = args)
+ if (logging) {
+ log.info(cmdRes.toString())
+ }
+ session.close()
+
+ return cmdRes
+
+ } catch (e: Exception) {
+ try {
+ session?.close()
+ } finally {
+ // nothing to do
+ }
+ return ProcessResult(
+ -1,
+ err = "Error when opening or executing remote ssh session (Pls check host, user, password resp. ssh key) - ",
+ ex = e
+ )
+ }
+ }
+
+ override fun close() {
+ try {
+ log.info("Disconnecting ssh.")
+ ssh.disconnect()
+ } catch (e: IOException) {
+ // No prov required
+ }
+ }
+
+ protected fun finalize() {
+ close()
+ }
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/core/tags/Api.kt b/bin/main/org/domaindrivenarchitecture/provs/core/tags/Api.kt
new file mode 100644
index 0000000..9d9bd08
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/core/tags/Api.kt
@@ -0,0 +1,6 @@
+package org.domaindrivenarchitecture.provs.core.tags
+
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION)
+@Retention(AnnotationRetention.SOURCE)
+@Suppress("unused") // element is offered to be used externally
+annotation class Api
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/demos/HelloWorld.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/demos/HelloWorld.kt
new file mode 100644
index 0000000..38c17dd
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/demos/HelloWorld.kt
@@ -0,0 +1,14 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.demos
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.local
+
+
+fun Prov.helloWorld() = def {
+ cmd("echo Hello world!")
+}
+
+
+fun main() {
+ local().helloWorld()
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/demos/SystemInfos.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/demos/SystemInfos.kt
new file mode 100644
index 0000000..3859fb1
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/demos/SystemInfos.kt
@@ -0,0 +1,54 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.demos
+
+import org.domaindrivenarchitecture.provs.core.*
+
+
+/**
+ * Prints some information and settings of the operating system and environment.
+ *
+ * For running locally no arguments are required.
+ * For running remotely either 2 or 3 arguments must be provided:
+ * either host and user for connection by ssh key ()
+ * or host, user and password for password-authenticated connection.
+ * E.g. 172.0.0.123 username or 172.0.0.123 username password
+ */
+fun main(vararg args: String) {
+ if (args.isEmpty()) {
+ local().printInfos()
+ } else {
+ if (args.size !in 2..3) {
+ println("Wrong number of arguments. Please specify either host and user if connection is done by ssh key or otherwise host, user and password. E.g. 172.0.0.123 username password")
+ } else {
+ val password = if (args.size == 2) null else Secret(args[3])
+ remote(args[0], args[1], password = password).printInfos()
+ }
+ }
+}
+
+
+fun Prov.printInfos() = def {
+ println("\nUbuntu Version:\n${ubuntuVersion()}")
+ println("\nCurrent directory:\n${currentDir()}")
+ println("\nTime zone:\n${timeZone()}")
+
+ val dir = cmd("pwd").out
+ println("\nCurrent directory: $dir")
+
+ ProvResult(true)
+}
+
+
+fun Prov.ubuntuVersion(): String? {
+ return cmd("lsb_release -a").out
+}
+
+
+fun Prov.currentDir(): String? {
+ return cmd("pwd").out
+}
+
+
+fun Prov.timeZone(): String? {
+ return cmd("cat /etc/timezone").out
+}
+
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_compounds/monitoring/ProvisionMonitoring.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_compounds/monitoring/ProvisionMonitoring.kt
new file mode 100644
index 0000000..958dea5
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_compounds/monitoring/ProvisionMonitoring.kt
@@ -0,0 +1,18 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_compounds.monitoring
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.base.NginxConf
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.base.nginxHttpConf
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.provisionNginxStandAlone
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.prometheus.base.configurePrometheusDocker
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.prometheus.base.runPrometheusDocker
+
+
+@Suppress("unused") // used externally
+fun Prov.provisionMonitoring() = requireAll {
+ configurePrometheusDocker()
+ runPrometheusDocker()
+ provisionNginxStandAlone(NginxConf.nginxHttpConf())
+}
+
+
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_compounds/monitoring/ProvisionNginxAndMonitoring.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_compounds/monitoring/ProvisionNginxAndMonitoring.kt
new file mode 100644
index 0000000..b19b98e
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_compounds/monitoring/ProvisionNginxAndMonitoring.kt
@@ -0,0 +1,20 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_compounds.monitoring
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.base.NginxConf
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.base.nginxAddLocation
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.base.nginxCreateSelfSignedCertificate
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.base.nginxHttpsConfWithLocationFiles
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.provisionNginxStandAlone
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.prometheus.base.prometheusNginxConfig
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.prometheus.provisionPrometheusDocker
+
+
+@Suppress("unused") // used externally
+fun Prov.provisionNginxMonitoring(nginxHost: String = "localhost") = def {
+ provisionPrometheusDocker(nginxHost)
+ nginxCreateSelfSignedCertificate()
+ provisionNginxStandAlone(NginxConf.nginxHttpsConfWithLocationFiles())
+ nginxAddLocation("443", nginxHost, "/prometheus", prometheusNginxConfig)
+}
+
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/certbot/ProvisionCertbot.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/certbot/ProvisionCertbot.kt
new file mode 100644
index 0000000..edffb3b
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/certbot/ProvisionCertbot.kt
@@ -0,0 +1,28 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.certbot
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.fileExists
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+
+
+/**
+ * Provisions a certbot for the specified serverName and email to obtain and renew letsencrypt certificates
+ * Parameter can be used to specify certbot options e.g. "--nginx" to configure nginx, see https://certbot.eff.org/docs/using.html#certbot-command-line-options
+ */
+fun Prov.provisionCertbot(serverName: String, email: String?, additionalOptions: String? = "") = requireAll {
+ aptInstall("snapd")
+ sh("""
+ sudo snap install core; sudo snap refresh core
+ sudo snap install --classic certbot
+ """.trimIndent())
+
+ if (!fileExists("/usr/bin/certbot")) {
+ cmd("sudo ln -s /snap/bin/certbot /usr/bin/certbot")
+ val emailOption = email?.let { " -m $it" } ?: "--register-unsafely-without-email"
+ cmd("sudo certbot $additionalOptions -n --agree-tos $emailOption -d $serverName")
+ } else {
+ ProvResult(true)
+ }
+
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/firewall/ProvisionFirewall.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/firewall/ProvisionFirewall.kt
new file mode 100644
index 0000000..6697463
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/firewall/ProvisionFirewall.kt
@@ -0,0 +1,143 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.firewall
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+
+
+fun Prov.saveIpTables() = requireAll {
+ sh("""
+ iptables-save > /etc/iptables/rules.v4
+ ip6tables-save > /etc/iptables/rules.v6
+
+ netfilter-persistent save""",
+ sudo = true)
+}
+
+
+fun Prov.makeIpTablesPersistent() = requireAll {
+ // inspired by https://gist.github.com/alonisser/a2c19f5362c2091ac1e7
+ // enables iptables-persistent to be installed without manual input
+ sh("""
+ echo iptables-persistent iptables-persistent/autosave_v4 boolean true | sudo debconf-set-selections
+ echo iptables-persistent iptables-persistent/autosave_v6 boolean true | sudo debconf-set-selections
+ """.trimIndent())
+
+ aptInstall("iptables-persistent netfilter-persistent")
+ saveIpTables()
+}
+
+
+fun Prov.resetFirewall() = requireAll {
+ sh("""
+ #!/bin/bash
+ sudo iptables -F
+ sudo iptables -X
+ sudo iptables -t nat -F
+ sudo iptables -t nat -X
+ sudo iptables -t mangle -F
+ sudo iptables -t mangle -X
+
+ # the rules allow us to reconnect by opening up all traffic.
+ sudo iptables -P INPUT ACCEPT
+ sudo iptables -P FORWARD ACCEPT
+ sudo iptables -P OUTPUT ACCEPT
+
+ # print out all rules to the console after running this file.
+ sudo iptables -nL
+ """, sudo = true
+ )
+}
+
+
+fun Prov.provisionFirewall(addNetworkProtections: Boolean = false) = requireAll {
+ if (addNetworkProtections) {
+ networkProtections()
+ }
+
+ // inspired by: https://github.com/ChrisTitusTech/firewallsetup/blob/master/firewall
+ sh("""
+ # Firewall
+
+ # Accept all traffic first to avoid ssh lockdown via iptables firewall rules #
+ iptables -P INPUT ACCEPT
+ iptables -P FORWARD ACCEPT
+ iptables -P OUTPUT ACCEPT
+
+ # Flush all chains
+ iptables --flush
+
+ # Allow unlimited traffic on the loopback interface
+ iptables -A INPUT -i lo -j ACCEPT
+ iptables -A OUTPUT -o lo -j ACCEPT
+
+ # Previously initiated and accepted exchanges bypass rule checking
+ # Allow unlimited outbound traffic
+ iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
+ iptables -A OUTPUT -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
+
+ #Ratelimit SSH for attack protection
+ iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 -j DROP
+ iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set
+ iptables -A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT
+
+ # Allow http/https ports to be accessible from the outside
+ iptables -A INPUT -p tcp --dport 80 -m state --state NEW -j ACCEPT # http
+ iptables -A INPUT -p tcp --dport 443 -m state --state NEW -j ACCEPT # https
+
+ # UDP packet rule. This is just a random udp packet rule as an example only
+ # iptables -A INPUT -p udp --dport 5021 -m state --state NEW -j ACCEPT
+
+ # Allow pinging of your server
+ iptables -A INPUT -p icmp --icmp-type 8 -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
+
+
+ # Drop all other traffic
+ iptables -A INPUT -j DROP
+
+ # print the activated rules to the console when script is completed
+ iptables -nL
+
+ # Set default policies
+ iptables --policy INPUT DROP
+ iptables --policy OUTPUT DROP
+ iptables --policy FORWARD DROP
+ """, sudo = true)
+ if (chk("docker -v")) {
+ ipTablesRecreateDockerRules()
+ } else {
+ ProvResult(true, "No need to create iptables docker rules as no docker installed.")
+ }
+}
+
+
+fun Prov.networkProtections() = def {
+ sh("""
+ # Drop ICMP echo-request messages sent to broadcast or multicast addresses
+ echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_broadcasts
+
+ # Drop source routed packets
+ echo 0 > /proc/sys/net/ipv4/conf/all/accept_source_route
+
+ # Enable TCP SYN cookie protection from SYN floods
+ echo 1 > /proc/sys/net/ipv4/tcp_syncookies
+
+ # Don't accept ICMP redirect messages
+ echo 0 > /proc/sys/net/ipv4/conf/all/accept_redirects
+
+ # Don't send ICMP redirect messages
+ echo 0 > /proc/sys/net/ipv4/conf/all/send_redirects
+
+ # Enable source address spoofing protection
+ echo 1 > /proc/sys/net/ipv4/conf/all/rp_filter
+
+ # Log packets with impossible source addresses
+ echo 1 > /proc/sys/net/ipv4/conf/all/log_martians
+ """.trimIndent())
+}
+
+
+fun Prov.ipTablesRecreateDockerRules() = requireAll {
+ // see https://stackoverflow.com/questions/25917941/docker-how-to-re-create-dockers-additional-iptables-rules
+ cmd("sudo service docker restart")
+}
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/firewall/base/FirewallBackup.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/firewall/base/FirewallBackup.kt
new file mode 100644
index 0000000..558b555
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/firewall/base/FirewallBackup.kt
@@ -0,0 +1,21 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.firewall.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+
+
+fun Prov.saveIpTablesToFile() = def {
+ val dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("-yyyy-MM-dd--HH:mm:ss"))
+ val file = "savedrules$dateTime.txt"
+ sh("""
+ sudo iptables-save > $file
+ cat $file""")
+}
+
+fun Prov.restoreIpTablesFromFile(file: String? = null) = def {
+ val fileName = file ?: cmd("ls -r a* | head -1\n").out
+ fileName?.let { cmd("sudo iptables-restore < $file") }
+ ?: ProvResult(false, err = "File to restore not found.")
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nexus/ProvisionNexus.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nexus/ProvisionNexus.kt
new file mode 100644
index 0000000..27b1a6b
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nexus/ProvisionNexus.kt
@@ -0,0 +1,92 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nexus
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.core.docker.containerRuns
+import org.domaindrivenarchitecture.provs.core.remote
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.fileExists
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+import org.domaindrivenarchitecture.provs.ubuntu.user.base.createUser
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.certbot.provisionCertbot
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.base.NginxConf
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.base.nginxReverseProxyHttpConfig
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.provisionNginxStandAlone
+
+
+/**
+ * Provisions sonatype nexus in a docker container.
+ * If you would want nexus to be accessible directly from the internet (e.g. for test or demo reasons)
+ * set parameter portAccessibleFromNetwork to true.
+ */
+fun Prov.provisionNexusWithDocker(portAccessibleFromNetwork: Boolean = false) = requireAll {
+ // https://blog.sonatype.com/sonatype-nexus-installation-using-docker
+ // https://medium.com/@AhGh/how-to-setup-sonatype-nexus-3-repository-manager-using-docker-7ff89bc311ce
+ aptInstall("docker.io")
+
+ if (!containerRuns("nexus")) {
+ val volume = "nexus-data"
+ if (!cmdNoEval("docker volume inspect $volume").success) {
+ cmd("docker volume create --name $volume")
+ }
+ cmd("sudo docker run -d --restart unless-stopped -p 8081:8081 --name nexus -v nexus-data:/nexus-data sonatype/nexus3")
+
+ for (n in 0..3) {
+ if (fileExists("/var/lib/docker/volumes/$volume/_data/admin.password", sudo = true)) {
+ val res = cmd("sudo cat /var/lib/docker/volumes/$volume/_data/admin.password")
+ println("Admin Password:" + res.out)
+ break
+ }
+ Thread.sleep(20000)
+ }
+ }
+ if (!portAccessibleFromNetwork) {
+ val netIf = getDefaultNetworkingInterface()
+ netIf?.also {
+ val iptablesParameters = "DOCKER-USER -i $it ! -s 127.0.0.1 -j DROP"
+ if (!cmdNoEval("sudo iptables -C $iptablesParameters").success) {
+ cmd("sudo iptables -I $iptablesParameters")
+ }
+ }
+ }
+ ProvResult(true) // dummy
+}
+
+private fun Prov.getDefaultNetworkingInterface(): String? {
+ return cmd("route | grep \"^default\" | grep -o \"[^ ]*\$\"\n").out?.trim()
+}
+
+
+/**
+ * Provisions sonatype nexus on the specified host.
+ * Creates user "nexus" on the remote system.
+ * Installs nexus in a docker container behind an nginx reverse proxy with ssl using letsencrypt certificates.
+ *
+ * To run this method it is required to have ssl root access to the host.
+ */
+@Suppress("unused") // to be used externally
+fun provisionNexusServer(serverName: String, certbotEmail: String) {
+ val userName = "nexus" + 7
+ remote(serverName, "root").def {
+ createUser(userName, copyAuthorizedKeysFromCurrentUser = true, sudo = true)
+ }
+ remote(serverName, userName).requireAll {
+ provisionNexusWithDocker()
+
+ if (provisionNginxStandAlone(NginxConf.nginxReverseProxyHttpConfig(serverName)).success) {
+
+ cmd("sudo cat /etc/nginx/nginx.conf")
+
+ provisionCertbot(serverName, certbotEmail, "--nginx")
+
+ optional {
+ cmd("sudo cat /etc/nginx/nginx.conf")
+ cmd("sudo sed -i 's/X-Forwarded-Proto \"http\"/X-Forwarded-Proto \"https\"/g' /etc/nginx/nginx.conf")
+ cmd("sudo systemctl reload nginx")
+ }
+ } else {
+ ProvResult(true)
+ }
+ }
+}
+
+
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nexus/base/NginxConf.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nexus/base/NginxConf.kt
new file mode 100644
index 0000000..e9adf04
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nexus/base/NginxConf.kt
@@ -0,0 +1,83 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nexus.base
+
+fun reverseProxyConfigHttpPort80(serverName: String): String {
+ // see https://help.sonatype.com/repomanager3/installation/run-behind-a-reverse-proxy
+ return """
+ events {} # event context have to be defined to consider config valid
+
+ http {
+
+ proxy_send_timeout 120;
+ proxy_read_timeout 300;
+ proxy_buffering off;
+ keepalive_timeout 5 5;
+ tcp_nodelay on;
+
+ server {
+ listen 80;
+ server_name $serverName;
+
+ # allow large uploads of files
+ client_max_body_size 1G;
+
+ # optimize downloading files larger than 1G
+ #proxy_max_temp_file_size 2G;
+
+ location / {
+ # Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup
+ proxy_pass http://127.0.0.1:8081/;
+ proxy_set_header Host ${'$'}host;
+ proxy_set_header X-Real-IP ${'$'}remote_addr;
+ proxy_set_header X-Forwarded-For ${'$'}proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto "http";
+ }
+ }
+ }
+ """.trimIndent()
+}
+
+
+fun reverseProxyConfigSsl(serverName: String, ssl_certificate: String? = null, ssl_certificate_key: String? = null): String {
+ // see https://help.sonatype.com/repomanager3/installation/run-behind-a-reverse-proxy
+
+ val sslCertificateEntry = ssl_certificate?.let { "ssl_certificate $ssl_certificate;" } ?: "ssl_certificate /etc/letsencrypt/live/$serverName/fullchain.pem;"
+ val sslCertificateKeyEntry = ssl_certificate?.let { "ssl_certificate_key $ssl_certificate_key;" } ?: "ssl_certificate_key /etc/letsencrypt/live/$serverName/privkey.pem"
+
+ return """
+events {} # event context have to be defined to consider config valid
+
+http {
+
+ proxy_send_timeout 120;
+ proxy_read_timeout 300;
+ proxy_buffering off;
+ keepalive_timeout 5 5;
+ tcp_nodelay on;
+
+ server {
+ listen *:443 ssl;
+ server_name $serverName;
+
+ # allow large uploads of files
+ client_max_body_size 1G;
+
+ # optimize downloading files larger than 1G
+ # proxy_max_temp_file_size 2G;
+
+ $sslCertificateEntry
+ $sslCertificateKeyEntry
+
+ location / {
+ # Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup
+ proxy_pass http://127.0.0.1:8081/;
+ proxy_set_header Host ${'$'}host;
+ proxy_set_header X-Real-IP ${'$'}remote_addr;
+ proxy_set_header X-Forwarded-For ${'$'}proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto "https";
+ }
+ }
+}
+ """
+}
+
+
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/ProvisionNginx.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/ProvisionNginx.kt
new file mode 100644
index 0000000..0c0e25e
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/ProvisionNginx.kt
@@ -0,0 +1,46 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.core.remote
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createFile
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.fileExists
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.base.NginxConf
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.base.createNginxLocationFolders
+import kotlin.system.exitProcess
+
+
+internal const val configFile = "/etc/nginx/nginx.conf"
+
+
+fun Prov.provisionNginxStandAlone(config: NginxConf? = null) = requireAll {
+
+ aptInstall("nginx")
+
+ createNginxLocationFolders()
+
+ if (config != null) {
+ if (fileExists(configFile)) {
+ cmd("sudo mv $configFile $configFile-orig")
+ }
+ createFile(configFile, config.conf, sudo = true)
+ val configCheck = cmd("sudo nginx -t")
+ if (configCheck.success) {
+ cmd("sudo service nginx restart")
+ } else {
+ ProvResult(false, err = "Nginx config is incorrect:\n" + configCheck.err)
+ }
+ } else {
+ ProvResult(true) // dummy
+ }
+}
+
+
+fun provisionRemote(vararg args: String) {
+ if (args.size != 2) {
+ println("Pls specify host and user for remote installation of nginx.")
+ exitProcess(1)
+ }
+ remote(args[0], args[1]).provisionNginxStandAlone()
+}
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/base/BasicAuth.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/base/BasicAuth.kt
new file mode 100644
index 0000000..ce692c1
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/base/BasicAuth.kt
@@ -0,0 +1,12 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.Secret
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+
+fun Prov.nginxAddBasicAuth(user: String, password: Secret) = requireAll {
+ aptInstall("apache2-utils")
+ val passwordFile = "/etc/nginx/.htpasswd"
+ cmdNoLog("sudo htpasswd -b -c $passwordFile $user ${password.plain()}")
+}
+
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/base/Configs.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/base/Configs.kt
new file mode 100644
index 0000000..97199d9
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/base/Configs.kt
@@ -0,0 +1,162 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.base
+
+class NginxConf(val conf: String = NGINX_MINIMAL_CONF) {
+ companion object {}
+}
+
+
+const val NGINX_MINIMAL_CONF = """
+events {}
+
+http {
+ server {
+ listen 80;
+
+ location / {
+ return 200 'Hi from nginx!';
+ }
+ }
+}
+"""
+
+
+@Suppress("unused") // use later
+fun NginxConf.Companion.nginxHttpConf(
+ serverName: String = "localhost"
+): NginxConf {
+ return NginxConf(
+ """
+events {}
+
+http {
+ server {
+ listen 80;
+ server_name $serverName;
+
+ include /etc/nginx/locations-enabled/port80*$locationsFileExtension;
+ }
+}
+"""
+ )
+}
+
+
+fun NginxConf.Companion.nginxHttpsConfWithLocationFiles(
+ sslCertificate: String = "/etc/nginx/ssl/cert/selfsigned.crt",
+ sslCertificateKey: String = "/etc/nginx/ssl/private/selfsigned.key"
+): NginxConf {
+ return NginxConf(
+ """
+events {}
+
+http {
+ server {
+ listen 443 ssl;
+ server_name localhost;
+
+ ssl_certificate $sslCertificate;
+ ssl_certificate_key $sslCertificateKey;
+
+ include /etc/nginx/locations-enabled/port443*$locationsFileExtension;
+ }
+}
+"""
+ )
+}
+
+
+@Suppress("unused") // use later
+fun NginxConf.Companion.nginxReverseProxySslConfig(
+ serverName: String,
+ ssl_certificate: String? = null,
+ ssl_certificate_key: String? = null
+): NginxConf {
+ // see https://help.sonatype.com/repomanager3/installation/run-behind-a-reverse-proxy
+
+ val sslCertificateEntry = ssl_certificate?.let { "ssl_certificate $ssl_certificate;" }
+ ?: "ssl_certificate /etc/letsencrypt/live/$serverName/fullchain.pem;"
+ val sslCertificateKeyEntry = ssl_certificate?.let { "ssl_certificate_key $ssl_certificate_key;" }
+ ?: "ssl_certificate_key /etc/letsencrypt/live/$serverName/privkey.pem"
+
+ return NginxConf(
+ """
+events {} # event context have to be defined to consider config valid
+
+http {
+
+ proxy_send_timeout 120;
+ proxy_read_timeout 300;
+ proxy_buffering off;
+ keepalive_timeout 5 5;
+ tcp_nodelay on;
+
+ server {
+ listen *:443 ssl;
+ server_name $serverName;
+
+ # allow large uploads of files
+ client_max_body_size 1G;
+
+ # optimize downloading files larger than 1G
+ #proxy_max_temp_file_size 2G;
+
+ $sslCertificateEntry
+ $sslCertificateKeyEntry
+
+ location / {
+ # Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup
+ proxy_pass http://127.0.0.1:8081/;
+ proxy_set_header Host ${'$'}host;
+ proxy_set_header X-Real-IP ${'$'}remote_addr;
+ proxy_set_header X-Forwarded-For ${'$'}proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto "https";
+ }
+ }
+}
+ """
+ )
+}
+
+
+@Suppress("unused") // use later
+fun NginxConf.Companion.nginxReverseProxyHttpConfig(
+ serverName: String
+): NginxConf {
+ // see https://help.sonatype.com/repomanager3/installation/run-behind-a-reverse-proxy
+
+ return NginxConf(
+ """
+events {} # event context have to be defined to consider config valid
+
+http {
+
+ proxy_send_timeout 120;
+ proxy_read_timeout 300;
+ proxy_buffering off;
+ keepalive_timeout 5 5;
+ tcp_nodelay on;
+
+ server {
+ listen *:80;
+ server_name $serverName;
+
+ # allow large uploads of files
+ client_max_body_size 1G;
+
+ # optimize downloading files larger than 1G
+ #proxy_max_temp_file_size 2G;
+
+ location / {
+ # Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup
+ proxy_pass http://127.0.0.1:8081/;
+ proxy_set_header Host ${'$'}host;
+ proxy_set_header X-Real-IP ${'$'}remote_addr;
+ proxy_set_header X-Forwarded-For ${'$'}proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto "https";
+ }
+ }
+}
+ """
+ )
+}
+
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/base/Locations.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/base/Locations.kt
new file mode 100644
index 0000000..dbe2bc5
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/base/Locations.kt
@@ -0,0 +1,44 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.*
+
+
+internal const val locationsAvailableDir = "/etc/nginx/locations-available/"
+internal const val locationsEnabledDir = "/etc/nginx/locations-enabled/"
+internal const val locationsFileExtension = ".locations"
+
+
+fun Prov.createNginxLocationFolders() = requireAll {
+ createDirs(locationsEnabledDir, sudo = true)
+ createDirs(locationsAvailableDir, sudo = true)
+}
+
+
+fun Prov.nginxIncludeLocationFolders() = requireAll {
+ replaceTextInFile("/etc/nginx/nginx.conf", "listen 80;\n",
+ """listen 80;
+ include ${locationsAvailableDir}port80*$locationsFileExtension;
+ include ${locationsEnabledDir}port443*$locationsFileExtension;
+ """)
+}
+
+
+fun Prov.nginxAddLocation(port: String, locationFileName: String, urlPath: String, content: String) = requireAll {
+
+ val locationConf = """location $urlPath {""" +
+ content +
+ "\n}"
+
+ if (!dirExists(locationsAvailableDir, sudo = true)) {
+ createNginxLocationFolders()
+ }
+
+ createFile("${locationsAvailableDir}port${port}_$locationFileName$locationsFileExtension", locationConf, sudo = true)
+ if (!fileExists("${locationsEnabledDir}port${port}_$locationFileName$locationsFileExtension", sudo = true)) {
+ cmd("sudo ln -s ${locationsAvailableDir}port${port}_$locationFileName$locationsFileExtension ${locationsEnabledDir}port${port}_$locationFileName$locationsFileExtension ")
+ } else {
+ ProvResult(true)
+ }
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/base/SelfSignedCertificate.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/base/SelfSignedCertificate.kt
new file mode 100644
index 0000000..5b6a469
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/base/SelfSignedCertificate.kt
@@ -0,0 +1,36 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createDirs
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.provisionNginxStandAlone
+
+
+internal val certificateName = "selfsigned"
+internal val sslDays = 365
+val dirSslCert="/etc/nginx/ssl/cert"
+val dirSslKey="/etc/nginx/ssl/private"
+
+
+fun Prov.nginxCreateSelfSignedCertificate(
+ country: String = "DE",
+ state: String = "test",
+ locality: String = "test",
+ organization: String = "test",
+ organizationalUnit: String = "test",
+ commonName: String = "test",
+ email : String = "test@test.net"
+) = def {
+ // inspired by https://gist.github.com/adrianorsouza/2bbfe5e197ce1c0b97c8
+ createDirs(dirSslCert, sudo = true)
+ createDirs(dirSslKey, sudo = true)
+ cmd("cd $dirSslKey && sudo openssl req -x509 -nodes -newkey rsa:2048 -keyout $certificateName.key -out $certificateName.crt -days $sslDays -subj \"/C=$country/ST=$state/L=$locality/O=$organization/OU=$organizationalUnit/CN=$commonName/emailAddress=$email\"")
+ cmd("sudo mv $dirSslKey/$certificateName.crt $dirSslCert/")
+}
+
+
+fun Prov.configureNginxWithSelfSignedCertificate() = def {
+ // todo: should not call provisionNginxStandAlone, which is defined in the package above
+ provisionNginxStandAlone(NginxConf.nginxReverseProxySslConfig("localhost",
+ dirSslCert+"/"+ certificateName + ".crt",
+ dirSslKey + "/" + certificateName + ".key"))
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/prometheus/ProvisionPrometheus.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/prometheus/ProvisionPrometheus.kt
new file mode 100644
index 0000000..9f33e83
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/prometheus/ProvisionPrometheus.kt
@@ -0,0 +1,19 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.prometheus
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.prometheus.base.*
+
+/**
+ * Provisions prometheus monitoring.
+ * If running behind an nginx, pls specify the hostname in parameter nginxHost (e.g. mydomain.com).
+ * To run it without nodeExporter (which provides system data to prometheus), set withNodeExporter to false.
+ */
+fun Prov.provisionPrometheusDocker(nginxHost: String? = null, withNodeExporter: Boolean = true) = def {
+ configurePrometheusDocker()
+ if (withNodeExporter) {
+ installNodeExporter()
+ runNodeExporter()
+ addNodeExporterToPrometheusConf()
+ }
+ runPrometheusDocker(nginxHost)
+}
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/prometheus/base/NodeExporter.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/prometheus/base/NodeExporter.kt
new file mode 100644
index 0000000..ad7450f
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/prometheus/base/NodeExporter.kt
@@ -0,0 +1,84 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.prometheus.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createDir
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createFile
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.fileContainsText
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.replaceTextInFile
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+import org.domaindrivenarchitecture.provs.ubuntu.user.base.whoami
+
+
+internal val defaultInstallationDir = "/usr/local/bin/"
+
+
+fun Prov.installNodeExporter() = requireAll {
+ // inspired by https://devopscube.com/monitor-linux-servers-prometheus-node-exporter/ and
+ // https://www.howtoforge.com/tutorial/how-to-install-prometheus-and-node-exporter-on-centos-8/#step-install-and-configure-nodeexporter
+ val downloadFileBasename = "node_exporter-1.0.1.linux-amd64"
+ val downloadFile = "$downloadFileBasename.tar.gz"
+ val downloadPath = "~/tmp/"
+ val fqFile = downloadPath + downloadFile
+
+ aptInstall("curl")
+ createDir("tmp")
+ sh(
+ """
+ cd tmp && curl -LO https://github.com/prometheus/node_exporter/releases/download/v1.0.1/$downloadFile --output $downloadFile
+ cd tmp && tar -xvf $fqFile -C $downloadPath
+
+ sudo mv $downloadPath$downloadFileBasename/node_exporter $defaultInstallationDir"""
+ )
+
+}
+
+
+fun Prov.runNodeExporter() = def {
+ createFile("/etc/systemd/system/node_exporter.service", nodeExporterServiceConf(whoami()?:"nouserfound"), sudo = true)
+
+ sh("""
+ sudo systemctl daemon-reload
+
+ # start the node_exporter service and enable it to launch everytime at system startup.
+ sudo systemctl start node_exporter
+ sudo systemctl enable node_exporter
+
+ # check if running
+ sudo systemctl status node_exporter --no-pager -l
+ """)
+}
+
+
+fun Prov.addNodeExporterToPrometheusConf (
+ prometheusConf: String = "/etc/prometheus/prometheus.yml",
+ sudo: Boolean = true
+) = requireAll {
+ val prometheusConfNodeExporter = """
+scrape_configs:
+ - job_name: 'node_exporter'
+ static_configs:
+ - targets: ['172.17.0.1:9100']
+"""
+ if (!fileContainsText(prometheusConf, "- job_name: 'node_exporter'", sudo)) {
+ replaceTextInFile(prometheusConf, "\nscrape_configs:\n", prometheusConfNodeExporter)
+ }
+ // cmd("sudo systemctl restart prometheus") for standalone
+ cmd("sudo docker restart prometheus")
+}
+
+
+fun nodeExporterServiceConf(user: String, installationDir: String = defaultInstallationDir): String {
+ return """
+[Unit]
+Description=Node Exporter
+Wants=network-online.target
+After=network-online.target
+
+[Service]
+User=$user
+ExecStart=${installationDir}node_exporter
+
+[Install]
+WantedBy=default.target
+"""
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/prometheus/base/Prometheus.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/prometheus/base/Prometheus.kt
new file mode 100644
index 0000000..42008c4
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/prometheus/base/Prometheus.kt
@@ -0,0 +1,75 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.prometheus.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.docker.containerRuns
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createDirs
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createFile
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+
+
+internal val configDir = "/etc/prometheus/"
+internal val configFile = "prometheus.yml"
+
+
+fun Prov.configurePrometheusDocker(config: String = prometheusDefaultConfig) = requireAll {
+ createDirs(configDir, sudo = true)
+ createFile(configDir + configFile, config, sudo = true)
+}
+
+
+fun Prov.runPrometheusDocker(nginxHost: String? = null) = requireAll {
+ aptInstall("docker.io")
+
+ val containerName = "prometheus"
+
+ if (containerRuns(containerName)) {
+ cmd("sudo docker restart $containerName")
+ } else {
+ if (nginxHost == null) {
+ cmd(
+ "sudo docker run -d -p 9090:9090 " +
+ " --name $containerName " +
+ " --restart on-failure:1" +
+ " -v prometheus-data:/prometheus" +
+ " -v $configDir$configFile:/etc/prometheus/prometheus.yml " +
+ " prom/prometheus"
+ )
+ } else {
+ cmd(
+ "sudo docker run -d -p 9090:9090 " +
+ " --name $containerName " +
+ " --restart on-failure:1" +
+ " -v prometheus-data:/prometheus" +
+ " -v $configDir$configFile:/etc/prometheus/prometheus.yml " +
+ " prom/prometheus --config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/prometheus " +
+ "--web.console.libraries=/usr/share/prometheus/console_libraries " +
+ "--web.console.templates=/usr/share/prometheus/consoles " +
+ "--web.external-url=http://$nginxHost/prometheus"
+ )
+ }
+ }
+}
+
+
+private const val prometheusDefaultConfig =
+ """
+global:
+ scrape_interval: 15s # By default, scrape targets every 15 seconds.
+
+ # Attach these labels to any time series or alerts when communicating with
+ # external systems (federation, remote storage, Alertmanager).
+ external_labels:
+ monitor: 'codelab-monitor'
+
+# A scrape configuration containing exactly one endpoint to scrape:
+# Here it's Prometheus itself.
+scrape_configs:
+ # The job name is added as a label `job=` to any timeseries scraped from this config.
+ - job_name: 'prometheus'
+
+ # Override the global default and scrape targets from this job every 5 seconds.
+ scrape_interval: 5s
+
+ static_configs:
+ - targets: ['localhost:9090']
+"""
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/prometheus/base/PrometheusNginxConfig.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/prometheus/base/PrometheusNginxConfig.kt
new file mode 100644
index 0000000..e4e1b39
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/prometheus/base/PrometheusNginxConfig.kt
@@ -0,0 +1,5 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.prometheus.base
+
+val prometheusNginxConfig = """
+ proxy_pass http://localhost:9090/prometheus;
+"""
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/ProvisionWorkplace.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/ProvisionWorkplace.kt
new file mode 100644
index 0000000..2cfb720
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/ProvisionWorkplace.kt
@@ -0,0 +1,155 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace
+
+import org.domaindrivenarchitecture.provs.core.*
+import org.domaindrivenarchitecture.provs.core.processors.RemoteProcessor
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace.base.*
+import org.domaindrivenarchitecture.provs.ubuntu.git.provisionGit
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstallFromPpa
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptPurge
+import org.domaindrivenarchitecture.provs.ubuntu.keys.KeyPair
+import org.domaindrivenarchitecture.provs.ubuntu.keys.base.gpgFingerprint
+import org.domaindrivenarchitecture.provs.ubuntu.keys.provisionKeysCurrentUser
+import org.domaindrivenarchitecture.provs.ubuntu.secret.secretSources.PromptSecretSource
+import org.domaindrivenarchitecture.provs.ubuntu.user.base.currentUserCanSudo
+import org.domaindrivenarchitecture.provs.ubuntu.user.base.makeUserSudoerWithNoSudoPasswordRequired
+import org.domaindrivenarchitecture.provs.ubuntu.user.base.whoami
+import java.net.InetAddress
+import kotlin.system.exitProcess
+
+
+enum class WorkplaceType {
+ MINIMAL, OFFICE, IDE
+}
+
+
+/**
+ * Provisions software and configurations for a personal workplace.
+ * Offers the possibility to choose between different types.
+ * Type OFFICE installs office-related software like Thunderbird, LibreOffice, and much more.
+ * Type IDE provides additional software for a development environment, such as Visual Studio Code, IntelliJ, etc.
+ *
+ * Prerequisites: user must be sudoer. If password is required for user to execute sudo, then also parameter userPassword must be provided
+ *
+ * @param workplaceType
+ * @param userPassword only needs to be provided if user cannot sudo without password
+ */
+fun Prov.provisionWorkplace(
+ workplaceType: WorkplaceType = WorkplaceType.MINIMAL,
+ ssh: KeyPair? = null,
+ gpg: KeyPair? = null,
+ gitUserName: String? = null,
+ gitEmail: String? = null,
+ userPassword: Secret? = null
+) = requireAll {
+
+ userPassword?.also { makeUserSudoerWithNoSudoPasswordRequired(it) }
+
+ if (!currentUserCanSudo()) {
+ throw Exception("Current user ${whoami()} cannot execute sudo without a password, but he must be able to in order to provisionWorkplace")
+ }
+
+ aptInstall("ssh gnupg curl git")
+
+ provisionKeysCurrentUser(gpg, ssh)
+ provisionGit(gitUserName ?: whoami(), gitEmail, gpg?.let { gpgFingerprint(it.publicKey.plain()) })
+
+ installVirtualBoxGuestAdditions()
+
+ aptPurge("remove-power-management xfce4-power-manager " +
+ "xfce4-power-manager-plugins xfce4-power-manager-data")
+ aptPurge("abiword gnumeric")
+ aptPurge("popularity-contest")
+
+ configureNoSwappiness()
+
+ if (workplaceType == WorkplaceType.OFFICE || workplaceType == WorkplaceType.IDE) {
+ aptInstall("seahorse")
+ aptInstall(BASH_UTILS)
+ aptInstall(OS_ANALYSIS)
+ aptInstall(ZIP_UTILS)
+
+ aptInstall("firefox chromium-browser")
+ aptInstall("thunderbird libreoffice")
+ aptInstall("xclip")
+
+ installZimWiki()
+ installGopass()
+ aptInstallFromPpa("nextcloud-devs", "client", "nextcloud-client")
+
+ aptInstall("inkscape")
+ aptInstall("dia")
+
+ aptInstall(SPELLCHECKING_DE)
+
+ installRedshift()
+ configureRedshift()
+ }
+
+ if (workplaceType == WorkplaceType.IDE) {
+
+ aptInstall(JAVA_JDK)
+
+ aptInstall(OPEN_VPM)
+ aptInstall(OPENCONNECT)
+ aptInstall(VPNC)
+
+ installDocker()
+
+ // IDEs
+ cmd("sudo snap install intellij-idea-community --classic")
+ installVSC("python", "clojure")
+ }
+
+ ProvResult(true) // dummy
+}
+
+
+/**
+ * Provisions a workplace on a remote machine.
+ * Prerequisite: you have built the uberjar by ./gradlew uberJarLatest
+ * The remote host and remote user are specified by args parameters.
+ * The first argument specifies hostName or IP-Address of the remote machine,
+ * the second argument defines the user on the remote machine for whom the workplace is provisioned;
+ * You can invoke this method e.g. using the jar-file from the project root by:
+ * java -jar build/libs/provs-extensions-uber.jar org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace.ProvisionWorkplaceKt provisionRemote
+ * You will be prompted for the password of the remote user.
+ *
+ * @param args host and userName of the remote machine as the first resp. second argument
+ */
+fun provisionRemote(args: Array) {
+ if (args.size != 2) {
+ println("Please specify host and user.")
+ exitProcess(1)
+ }
+
+ val host = InetAddress.getByName(args[0])
+ val userName = args[1]
+ val pwSecret = PromptSecretSource("Password for user $userName on $host").secret()
+ val pwFromSecret = Password(pwSecret.plain())
+
+ val config = readWorkplaceConfigFromFile() ?: WorkplaceConfig()
+ Prov.newInstance(RemoteProcessor(host, userName, pwFromSecret), OS.LINUX.name).provisionWorkplace(
+ config.type,
+ config.ssh?.keyPair(),
+ config.gpg?.keyPair(),
+ config.gitUserName,
+ config.gitEmail,
+ pwFromSecret
+ )
+}
+
+
+/**
+ * Provisions a workplace on a remote machine by calling method provisionRemote.
+ *
+ * @ see #provisionRemote(args: Array)
+ *
+ * You can invoke this method e.g. using the jar-file from the project root by:
+ * java -jar build/libs/provs-ext-latest.jar workplace.WorkplaceKt main
+ *
+ * @param args host and userName of the remote machine as first resp. second argument
+ */
+fun main(args: Array) {
+ provisionRemote(args = args)
+}
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/WorkplaceConfig.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/WorkplaceConfig.kt
new file mode 100644
index 0000000..1dd0c94
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/WorkplaceConfig.kt
@@ -0,0 +1,42 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace
+
+import org.domaindrivenarchitecture.provs.ubuntu.keys.KeyPairSource
+import org.domaindrivenarchitecture.provs.ubuntu.secret.SecretSource
+import org.domaindrivenarchitecture.provs.ubuntu.secret.SecretSourceType
+import org.domaindrivenarchitecture.provs.ubuntu.secret.SecretSupplier
+import org.domaindrivenarchitecture.provs.ubuntu.secret.secretSources.PlainSecretSource
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import java.io.*
+
+
+@Serializable
+class WorkplaceConfig(
+ val type: WorkplaceType = WorkplaceType.MINIMAL,
+ val ssh: KeyPairSource? = null,
+ val gpg: KeyPairSource? = null,
+ val gitUserName: String? = null,
+ val gitEmail: String? = null,
+)
+
+
+// -------------------------------------------- file methods ------------------------------------
+fun readWorkplaceConfigFromFile(filename: String = "WorkplaceConfig.json"): WorkplaceConfig? {
+ val file = File(filename)
+ return if (file.exists())
+ try {
+ // read from file
+ val inputAsString = BufferedReader(FileReader(filename)).use { it.readText() }
+
+ return Json.decodeFromString(WorkplaceConfig.serializer(), inputAsString)
+ } catch (e: FileNotFoundException) {
+ null
+ } else null
+}
+
+
+fun writeWorkplaceConfigToFile(config: WorkplaceConfig) {
+ val fileName = "WorkplaceConfig.json"
+
+ FileWriter(fileName).use { it.write(Json.encodeToString(WorkplaceConfig.serializer(), config)) }
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/Docker.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/Docker.kt
new file mode 100644
index 0000000..80fab6f
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/Docker.kt
@@ -0,0 +1,12 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+
+fun Prov.installDocker() = def {
+ aptInstall("containerd docker.io")
+ if (!chk("getent group docker")) {
+ cmd("sudo groupadd docker")
+ }
+ cmd("sudo gpasswd -a \$USER docker")
+}
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/Fakturama.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/Fakturama.kt
new file mode 100644
index 0000000..4f34429
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/Fakturama.kt
@@ -0,0 +1,17 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createDir
+import org.domaindrivenarchitecture.provs.ubuntu.web.base.downloadFromURL
+
+
+fun Prov.installFakturama() = def {
+ createDir("/tmp", sudo = true)
+ downloadFromURL( "https://files.fakturama.info/release/v2.1.1/Installer_Fakturama_linux_x64_2.1.1b.deb", "fakturama.deb", "/tmp")
+ cmd("sudo dpkg -i fakturama.deb", "/tmp")
+
+ createDir("/opt/fakturama", sudo = true)
+ val filename = "Handbuch-Fakturama_2.1.1.pdf"
+ downloadFromURL( "https://files.fakturama.info/release/v2.1.1/Handbuch-Fakturama_2.1.1.pdf", filename, "/tmp")
+ cmd("sudo mv /tmp/$filename /opt/fakturama")
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/Gopass.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/Gopass.kt
new file mode 100644
index 0000000..3ec54c3
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/Gopass.kt
@@ -0,0 +1,91 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createDir
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createDirs
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createFile
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.userHome
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.isPackageInstalled
+
+
+fun Prov.installGopass(version: String = "1.12.7", enforceVersion: Boolean = false) = def {
+ if (isPackageInstalled("gopass") && !enforceVersion) {
+ ProvResult(true)
+ } else {
+ // install required dependencies
+ aptInstall("rng-tools gnupg2 git")
+ aptInstall("curl")
+
+ sh(
+ """
+ curl -L https://github.com/gopasspw/gopass/releases/download/v${version}/gopass_${version}_linux_amd64.deb -o gopass_${version}_linux_amd64.deb
+ sudo dpkg -i gopass_${version}_linux_amd64.deb
+ """
+ )
+ gopassEnsureVersion(version)
+ }
+}
+
+
+fun Prov.configureGopass(gopassRootFolder: String? = null) = def {
+ val defaultRootFolder = userHome() + ".password-store"
+ val rootFolder = gopassRootFolder ?: defaultRootFolder
+ // use default
+ createDir(rootFolder)
+ createDirs(".config/gopass")
+ createFile("~/.config/gopass/config.yml", gopassConfig(rootFolder))
+}
+
+
+fun Prov.gopassMountStore(storeName: String, path: String, indexOfRecepientKey: Int = 0) = def {
+ cmd("printf \"$indexOfRecepientKey\\n\" | gopass mounts add $storeName $path")
+}
+
+
+internal fun gopassConfig(gopassRoot: String): String {
+ return """
+root:
+ askformore: false
+ autoclip: true
+ autoprint: false
+ autoimport: true
+ autosync: false
+ check_recipient_hash: false
+ cliptimeout: 45
+ concurrency: 1
+ editrecipients: false
+ exportkeys: true
+ nocolor: false
+ noconfirm: true
+ nopager: false
+ notifications: true
+ path: gpgcli-gitcli-fs+file://$gopassRoot
+ recipient_hash:
+ .gpg-id: 3078303637343130344341383141343930350aa69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26
+ safecontent: false
+ usesymbols: false
+mounts: {}
+ """.trim() + "\n"
+}
+
+
+/**
+ * Returns true if gopass is installed and has the given version.
+ *
+ * @param version that is checked; specifies left part of text of installed version, e.g. both "1" and "1.12" will return true if installed version is "1.12.6+8d7a311b9273846bbb618e4bd9ddbae51b1db7b8"
+ */
+internal fun Prov.gopassEnsureVersion(version: String) = def {
+ val installedGopassVersion = gopassVersion()
+ if (installedGopassVersion != null && installedGopassVersion.startsWith("gopass " + version)) {
+ ProvResult(true, out = "Required gopass version ($version) matches installed version ($installedGopassVersion)")
+ } else {
+ ProvResult(false, err = "Wrong gopass version. Expected $version but found $installedGopassVersion")
+ }
+}
+
+internal fun Prov.gopassVersion(): String? {
+ val result = cmdNoEval("gopass -v")
+ return if (!result.success) null else result.out
+}
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/GopassBridge.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/GopassBridge.kt
new file mode 100644
index 0000000..83d4188
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/GopassBridge.kt
@@ -0,0 +1,90 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createDir
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createDirs
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.userHome
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.isPackageInstalled
+import org.domaindrivenarchitecture.provs.ubuntu.web.base.downloadFromURL
+
+
+fun Prov.downloadGopassBridge() = def {
+ val version = "0.8.0"
+ val filename = "gopass_bridge-${version}-fx.xpi"
+ val downloadDir = "${userHome()}Downloads/"
+
+ createDirs(downloadDir)
+ downloadFromURL(
+ "-L https://addons.mozilla.org/firefox/downloads/file/3630534/" + filename,
+ downloadDir + filename
+ )
+ // needs manual install with: firefox Downloads/gopass_bridge-0.8.0-fx.xpi
+}
+
+fun Prov.installGopassBridgeJsonApi() = def {
+ // see https://github.com/gopasspw/gopass-jsonapi
+ val gopassBridgeVersion = "1.11.1"
+ val requiredGopassVersion = "1.12"
+ val filename = "gopass-jsonapi_${gopassBridgeVersion}_linux_amd64.deb"
+ val downloadUrl = "-L https://github.com/gopasspw/gopass-jsonapi/releases/download/v$gopassBridgeVersion/$filename"
+ val downloadDir = "${userHome()}Downloads"
+ val installedJsonApiVersion = gopassJsonApiVersion()?.trim()
+
+ if (installedJsonApiVersion == null) {
+ if (chk("gopass ls")) {
+ if (gopassEnsureVersion(requiredGopassVersion).success) {
+ aptInstall("git gnupg2") // required dependencies
+ createDir(downloadDir)
+ downloadFromURL(downloadUrl, filename, downloadDir)
+ cmd("dpkg -i " + downloadDir + "/" + filename, sudo = true)
+ } else {
+ ProvResult(
+ false,
+ "Version of currently installed gopass (" + gopassVersion() + ") is incompatible with gopass-jsonapi version to be installed. " +
+ "Please upgrade gopass to version: " + requiredGopassVersion
+ )
+ }
+ } else {
+ addResultToEval(
+ ProvResult(
+ false,
+ "gopass not initialized correctly. You can initialize gopass with: \"gopass init\""
+ )
+ )
+ }
+ } else {
+ if (installedJsonApiVersion.startsWith("gopass-jsonapi version " + gopassBridgeVersion)) {
+ addResultToEval(ProvResult(true, out = "Version $gopassBridgeVersion of gopass-jsonapi is already installed"))
+ } else {
+ addResultToEval(
+ ProvResult(
+ false,
+ err = "gopass-jsonapi (version $gopassBridgeVersion) cannot be installed as version $installedJsonApiVersion is already installed." +
+ " Upgrading gopass-jsonapi is currently not supported by provs."
+ )
+ )
+ }
+ }
+}
+
+fun Prov.configureGopassBridgeJsonApi() = def {
+ if (isPackageInstalled("gopass-jsonapi")) {
+ // configure for firefox and choose default for each:
+ // "Install for all users? [y/N/q]",
+ // "In which path should gopass_wrapper.sh be installed? [/home/testuser/.config/gopass]"
+ // "Wrapper Script for gopass_wrapper.sh ..."
+ cmd("printf \"\\n\\n\\n\" | gopass-jsonapi configure --browser firefox")
+ } else {
+ ProvResult(
+ false,
+ err = "gopass-jsonapi is missing. Gopass-jsonapi must be installed to be able to configure it."
+ )
+ }
+}
+
+internal fun Prov.gopassJsonApiVersion(): String? {
+ val result = cmdNoEval("gopass-jsonapi -v")
+ return if (!result.success) null else result.out
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/NoSwappiness.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/NoSwappiness.kt
new file mode 100644
index 0000000..23bc485
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/NoSwappiness.kt
@@ -0,0 +1,10 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.addTextToFile
+import java.io.File
+
+fun Prov.configureNoSwappiness() = def {
+ // set swappiness to 0
+ addTextToFile("vm.swappiness=0", File("/etc/sysctl.conf"), sudo = true)
+}
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/PackageBundles.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/PackageBundles.kt
new file mode 100644
index 0000000..49997dc
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/PackageBundles.kt
@@ -0,0 +1,17 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace.base
+
+val OS_ANALYSIS = "lsof strace ncdu iptraf htop iotop iftop"
+
+val ZIP_UTILS = "p7zip-rar p7zip-full rar unrar zip unzip"
+
+val BASH_UTILS = "bash-completion"
+
+val SPELLCHECKING_DE = "hyphen-de hunspell hunspell-de-de"
+
+val OPEN_VPM = "openvpn network-manager-openvpn network-manager-openvpn-gnome"
+
+val OPENCONNECT = "openconnect network-manager-openconnect network-manager-openconnect-gnome"
+
+val VPNC = "vpnc network-manager-vpnc network-manager-vpnc-gnome vpnc-scripts"
+
+val JAVA_JDK = "openjdk-8-jdk openjdk-11-jdk openjdk-14-jdk"
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/Redshift.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/Redshift.kt
new file mode 100644
index 0000000..0c8950f
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/Redshift.kt
@@ -0,0 +1,35 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createDir
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createFile
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+
+
+fun Prov.installRedshift() = def {
+ aptInstall("redshift redshift-gtk")
+}
+
+
+fun Prov.configureRedshift() = def {
+ aptInstall("redshift redshift-gtk")
+
+ createDir(".config")
+ createFile("~/.config/redshift.conf", config)
+}
+
+
+val config = """
+ [redshift]
+ temp-day=5500
+ temp-night=2700
+ brightness-day=1
+ brightness-night=0.6
+ fade=1
+
+ location-provider=manual
+
+ [manual]
+ lat=48.783333
+ lon=9.1833334
+""".trimIndent()
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/VSCode.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/VSCode.kt
new file mode 100644
index 0000000..1a92d8d
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/VSCode.kt
@@ -0,0 +1,75 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+
+
+fun Prov.installVSC(vararg options: String) = requireAll {
+ val clojureExtensions =
+ arrayListOf("betterthantomorrow.calva", "martinklepsch.clojure-joker-linter", "DavidAnson.vscode-markdownlint")
+ val pythonExtensions = arrayListOf("ms-python.python")
+
+ prerequisitesVSCinstall()
+
+ installVSCPackage()
+
+ if (options.contains("clojure")) {
+ installExtensions(clojureExtensions)
+ }
+ if (options.contains("python")) {
+ installExtensions(pythonExtensions)
+ }
+
+ provisionAdditionalTools()
+}
+
+
+private fun Prov.prerequisitesVSCinstall() = def {
+ aptInstall("curl gpg unzip apt-transport-https")
+}
+
+
+@Suppress("unused") // only required for installation of vscode via apt
+private fun Prov.configurePackageManagerForVsc() = requireAll {
+ // see https://code.visualstudio.com/docs/setup/linux
+ // alternatively install with snapd (but this cannot be tested within docker as snapd within docker is not working/supported)
+
+ sh(
+ """
+ curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg
+ sudo install -o root -g root -m 644 packages.microsoft.gpg /etc/apt/trusted.gpg.d/
+ sudo sh -c 'echo \"deb [arch=amd64 signed-by=/etc/apt/trusted.gpg.d/packages.microsoft.gpg] https://packages.microsoft.com/repos/vscode stable main\" > /etc/apt/sources.list.d/vscode.list'
+ """
+ )
+ aptInstall("apt-transport-https")
+ aptInstall("code")
+}
+
+
+private fun Prov.installVSCPackage() = def {
+ cmd("sudo snap install code --classic")
+
+ // to install via apt use:
+ // configurePackageManagerForVsc()
+ // aptInstall("code")
+
+}
+
+
+private fun Prov.installExtensions(extensions: List) = optional {
+ var res = ProvResult(true)
+ for (ext in extensions) {
+ res = cmd("code --install-extension $ext")
+ }
+ res
+ // Settings can be found at $HOME/.config/Code/User/settings.json
+}
+
+
+private fun Prov.provisionAdditionalTools() = requireAll {
+ // Joker
+ cmd("curl -Lo joker-0.12.2-linux-amd64.zip https://github.com/candid82/joker/releases/download/v0.12.2/joker-0.12.2-linux-amd64.zip")
+ cmd("unzip joker-0.12.2-linux-amd64.zip")
+ cmd("sudo mv joker /usr/local/bin/")
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/VirtualBoxGuest.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/VirtualBoxGuest.kt
new file mode 100644
index 0000000..6bd96be
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/VirtualBoxGuest.kt
@@ -0,0 +1,24 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+import org.domaindrivenarchitecture.provs.ubuntu.user.base.whoami
+
+fun Prov.installVirtualBoxGuestAdditions() = def {
+ // if running in a VirtualBox vm
+ if (!chk("lspci | grep VirtualBox")) {
+ return@def ProvResult(true, "Not running in a VirtualBox")
+ }
+
+ if (chk("VBoxService --version")) {
+ return@def ProvResult(true, "VBoxService already installed")
+ }
+
+ // install guest additions
+ cmd("sudo add-apt-repository multiverse")
+ aptInstall("virtualbox-guest-x11") // virtualbox-guest-dkms")
+ // and add user to group vboxsf e.g. to be able to use shared folders
+ whoami()?.let { cmd("sudo usermod -G vboxsf -a " + it) }
+ ?: ProvResult(true)
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/Zim.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/Zim.kt
new file mode 100644
index 0000000..42d3f4a
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/Zim.kt
@@ -0,0 +1,17 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstallFromPpa
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.isPackageInstalled
+
+
+fun Prov.installZimWiki() = def {
+ if (isPackageInstalled("zim")) {
+ ProvResult(true, out = "zim already installed.")
+ } else {
+ aptInstallFromPpa("jaap.karssenberg", "zim", "zim")
+ aptInstall("python3-gtkspellcheck")
+ }
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/filesystem/base/Filesystem.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/filesystem/base/Filesystem.kt
new file mode 100644
index 0000000..d97e4bd
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/filesystem/base/Filesystem.kt
@@ -0,0 +1,197 @@
+package org.domaindrivenarchitecture.provs.ubuntu.filesystem.base
+
+import org.domaindrivenarchitecture.provs.core.*
+import org.domaindrivenarchitecture.provs.core.platforms.SHELL
+import java.io.File
+
+
+fun Prov.fileExists(file: String, sudo: Boolean = false): Boolean {
+ return cmdNoEval((if (sudo) "sudo " else "") + "test -e " + file).success
+}
+
+
+fun Prov.createFile(
+ fullyQualifiedFilename: String,
+ text: String?,
+ posixFilePermission: String? = null,
+ sudo: Boolean = false
+): ProvResult =
+ def {
+ val withSudo = if (sudo) "sudo " else ""
+ posixFilePermission?.let {
+ ensureValidPosixFilePermission(posixFilePermission)
+ cmd(withSudo + "install -m $posixFilePermission /dev/null $fullyQualifiedFilename")
+ }
+ if (text != null) {
+ if (sudo) {
+ cmd(
+ "printf " + text.escapeProcentForPrintf()
+ .escapeAndEncloseByDoubleQuoteForShell() + " | sudo tee $fullyQualifiedFilename > /dev/null"
+ )
+ } else {
+ cmd(
+ "printf " + text.escapeProcentForPrintf()
+ .escapeAndEncloseByDoubleQuoteForShell() + " > $fullyQualifiedFilename"
+ )
+ }
+ } else {
+ cmd(withSudo + "touch $fullyQualifiedFilename")
+ }
+ }
+
+
+fun Prov.createSecretFile(
+ fullyQualifiedFilename: String,
+ secret: Secret,
+ posixFilePermission: String? = null
+): ProvResult =
+ def {
+ posixFilePermission?.let {
+ ensureValidPosixFilePermission(posixFilePermission)
+ cmd("install -m $posixFilePermission /dev/null $fullyQualifiedFilename")
+ }
+ cmdNoLog("echo '" + secret.plain().escapeSingleQuote() + "' > $fullyQualifiedFilename")
+ }
+
+
+fun Prov.deleteFile(file: String, sudo: Boolean = false): ProvResult = def {
+ cmd((if (sudo) "sudo " else "") + "rm $file")
+}
+
+
+fun Prov.fileContainsText(file: String, content: String, sudo: Boolean = false): Boolean {
+ return cmdNoEval((if (sudo) "sudo " else "") + "grep '${content.escapeSingleQuote()}' $file").success
+}
+
+
+fun Prov.fileContent(file: String, sudo: Boolean = false): String? {
+ return cmd((if (sudo) "sudo " else "") + "cat $file").out
+}
+
+
+fun Prov.addTextToFile(
+ text: String,
+ file: File,
+ doNotAddIfExisting: Boolean = true,
+ sudo: Boolean = false
+): ProvResult =
+ def {
+ // TODO find solution without trim handling spaces, newlines, etc correctly
+ val findCmd = "grep '${text.trim().escapeSingleQuote()}' ${file}"
+ val findResult = cmdNoEval(if (sudo) findCmd.sudoizeCommand() else findCmd)
+ if (!findResult.success || !doNotAddIfExisting) {
+ val addCmd = "printf \"" + text.escapeDoubleQuote() + "\" >> " + file
+ cmd(if (sudo) addCmd.sudoizeCommand() else addCmd)
+ } else {
+ ProvResult(true)
+ }
+ }
+
+
+fun Prov.replaceTextInFile(file: String, oldText: String, replacement: String) = def {
+ replaceTextInFile(file, Regex.fromLiteral(oldText), Regex.escapeReplacement(replacement))
+}
+
+
+fun Prov.replaceTextInFile(file: String, oldText: Regex, replacement: String) = def {
+ // todo: only use sudo for root or if owner different from current
+ val content = fileContent(file, true)
+ if (content != null) {
+ cmd("sudo truncate -s 0 $file")
+ addTextToFile(content.replace(oldText, Regex.escapeReplacement(replacement)), File(file), sudo = true)
+ } else {
+ ProvResult(false)
+ }
+}
+
+
+fun Prov.insertTextInFile(file: String, textBehindWhichToInsert: Regex, textToInsert: String) = def {
+ // todo: only use sudo for root or if owner different from current
+ val content = fileContent(file, true)
+ if (content != null) {
+ val match = textBehindWhichToInsert.find(content)
+ if (match != null) {
+ cmd("sudo truncate -s 0 $file")
+ addTextToFile(
+ content.replace(textBehindWhichToInsert, match.value + Regex.escapeReplacement(textToInsert)),
+ File(file),
+ sudo = true
+ )
+ } else {
+ ProvResult(false, err = "Text not found")
+ }
+ } else {
+ ProvResult(false)
+ }
+}
+
+
+fun Prov.dirExists(dir: String, path: String? = null, sudo: Boolean = false): Boolean {
+ val effectivePath = if (path != null) path else
+ (if (dir.startsWith(File.separator)) File.separator else "~" + File.separator)
+ val cmd = "cd $effectivePath && test -d $dir"
+ return cmdNoEval(if (sudo) cmd.sudoizeCommand() else cmd).success
+}
+
+
+fun Prov.createDir(
+ dir: String,
+ path: String = "~/",
+ failIfExisting: Boolean = false,
+ sudo: Boolean = false
+): ProvResult = def {
+ if (!failIfExisting && dirExists(dir, path, sudo)) {
+ ProvResult(true)
+ } else {
+ val cmd = "cd $path && mkdir $dir"
+ cmd(if (sudo) cmd.sudoizeCommand() else cmd)
+ }
+}
+
+
+fun Prov.createDirs(
+ dirs: String,
+ path: String = "~/",
+ failIfExisting: Boolean = false,
+ sudo: Boolean = false
+): ProvResult = def {
+ if (!failIfExisting && dirExists(dirs, path, sudo)) {
+ ProvResult(true)
+ } else {
+ val cmd = "cd $path && mkdir -p $dirs"
+ cmd(if (sudo) cmd.sudoizeCommand() else cmd)
+ }
+}
+
+
+fun Prov.deleteDir(dir: String, path: String, sudo: Boolean = false): ProvResult {
+ if ("" == path)
+ throw RuntimeException("In deleteDir: path must not be empty.")
+ val cmd = "cd $path && rmdir $dir"
+ return if (!sudo) {
+ cmd(cmd)
+ } else {
+ cmd(cmd.sudoizeCommand())
+ }
+}
+
+
+fun Prov.userHome(): String {
+ val user = cmd("whoami").out
+ if (user == null) {
+ throw RuntimeException("Could not determine user with whoami")
+ } else {
+ // assume default home folder
+ return "/home/" + user.trim() + "/"
+ }
+}
+
+
+private fun ensureValidPosixFilePermission(posixFilePermission: String) {
+ if (!Regex("^[0-7]{3}$").matches(posixFilePermission)) throw RuntimeException("Wrong file permission ($posixFilePermission), permission must consist of 3 digits as e.g. 664 ")
+}
+
+
+private fun String.sudoizeCommand(): String {
+ return "sudo " + SHELL + " -c " + this.escapeAndEncloseByDoubleQuoteForShell()
+}
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/git/ProvisionGit.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/git/ProvisionGit.kt
new file mode 100644
index 0000000..f96b57a
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/git/ProvisionGit.kt
@@ -0,0 +1,22 @@
+package org.domaindrivenarchitecture.provs.ubuntu.git
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+
+
+fun Prov.provisionGit(
+ userName: String? = null,
+ email: String? = null,
+ signingKey: String? = null,
+ diffTool: String? = null
+): ProvResult = def {
+
+ aptInstall("git")
+
+ cmd("git config --global push.default simple")
+ userName?.let { cmd("git config --global user.name $it") }
+ email?.let { cmd("git config --global user.email $it") }
+ signingKey?.let { cmd("git config --global user.signingkey $it") }
+ diffTool?.let { cmd("git config --global --add diff.tool $it") } ?: ProvResult(true)
+}
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/git/base/Git.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/git/base/Git.kt
new file mode 100644
index 0000000..ab127b7
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/git/base/Git.kt
@@ -0,0 +1,80 @@
+package org.domaindrivenarchitecture.provs.ubuntu.git.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.addTextToFile
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createDir
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.dirExists
+import org.domaindrivenarchitecture.provs.ubuntu.keys.base.isHostKnown
+import org.domaindrivenarchitecture.provs.ubuntu.utils.printToShell
+import java.io.File
+
+
+/**
+ * @param host name or ip
+ * @param rsaFingerprints
+ */
+private fun Prov.trustHost(host: String, rsaFingerprints: Set) = def {
+ if (!isHostKnown(host)) {
+ // logic based on https://serverfault.com/questions/447028/non-interactive-git-clone-ssh-fingerprint-prompt
+ val key = cmd("ssh-keyscan $host").out
+ if (key == null) {
+ ProvResult(false, "No key retrieved for $host")
+ } else {
+ val c = printToShell(key).trim()
+ val fpr = cmd(c + " | ssh-keygen -lf -").out
+ if (rsaFingerprints.contains(fpr)
+ ) {
+ createDir(".ssh", "~/")
+ cmd(printToShell(key) + " >> ~/.ssh/known_hosts")
+ } else {
+ ProvResult(false, "Fingerprint $fpr not valid for $host")
+ }
+ }
+ } else {
+ ProvResult(true)
+ }
+}
+
+
+fun Prov.gitClone(repo: String, path: String, pullIfExisting: Boolean = true): ProvResult = def {
+ val dir = cmdNoEval("basename $repo .git").out?.trim()
+ if (dir == null) {
+ ProvResult(false, err = "$repo is not a valid git repository")
+ } else {
+ val pathToDir = if (path.endsWith("/")) path + dir else path + "/" + dir
+ if (dirExists(pathToDir + "/.git/")) {
+ if (pullIfExisting) {
+ cmd("cd $pathToDir && git pull")
+ } else {
+ ProvResult(true, out = "Repo $repo is already existing")
+ }
+ } else {
+ cmd("cd $path && git clone $repo")
+ }
+ }
+}
+
+
+fun Prov.trustGithub() = def {
+ // current see https://docs.github.com/en/github/authenticating-to-github/githubs-ssh-key-fingerprints
+ val fingerprints = setOf(
+ "2048 SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8 github.com (RSA)\n",
+ "2048 SHA256:br9IjFspm1vxR3iA35FWE+4VTyz1hYVLIE2t1/CeyWQ github.com (RSA)\n"
+ )
+
+ trustHost("github.com", fingerprints)
+}
+
+
+fun Prov.trustGitlab() = def {
+ // from https://docs.gitlab.com/ee/user/gitlab_com/
+ val gitlabFingerprints = """
+ gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf
+ gitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9
+ gitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=
+ """.trimIndent()
+ addTextToFile("\n" + gitlabFingerprints+ "\n", File("~/.ssh/known_hosts"))
+}
+
+
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/install/base/Install.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/install/base/Install.kt
new file mode 100644
index 0000000..83c09af
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/install/base/Install.kt
@@ -0,0 +1,60 @@
+package org.domaindrivenarchitecture.provs.ubuntu.install.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+
+
+private var aptInit = false
+
+/**
+ * Installs package(s) by using package manager "apt".
+ *
+ * @param packages the packages to be installed, packages separated by space if there are more than one
+ */
+fun Prov.aptInstall(packages: String): ProvResult = def {
+ if (!aptInit) {
+ cmd("sudo apt-get update")
+ cmd("sudo apt-get install -qy apt-utils")
+ aptInit = true
+ }
+
+ val packageList = packages.split(" ")
+ for (packg in packageList) {
+ // see https://superuser.com/questions/164553/automatically-answer-yes-when-using-apt-get-install
+ cmd("sudo DEBIAN_FRONTEND=noninteractive apt-get install -qy $packg")
+ }
+ ProvResult(true) // dummy
+}
+
+
+/**
+ * Installs a package from a ppa (personal package archive) by using package manager "apt".
+ *
+ * @param packageName the package to install
+ */
+fun Prov.aptInstallFromPpa(launchPadUser: String, ppaName: String, packageName: String): ProvResult = def {
+ aptInstall("software-properties-common") // for being able to use add-apt-repository
+ cmd("sudo add-apt-repository -y ppa:$launchPadUser/$ppaName")
+ aptInstall(packageName)
+}
+
+
+/**
+ * Checks if a program is installed
+ *
+ * @param packageName to check
+ * @return true if program is installed
+ */
+@Suppress("unused") // used externally
+fun Prov.isPackageInstalled(packageName: String): Boolean {
+ return chk("timeout 2 dpkg -l $packageName")
+}
+
+
+/**
+ * Removes a package including its configuration and data files
+ */
+@Suppress("unused") // used externally
+fun Prov.aptPurge(packageName: String): Boolean {
+ return chk("sudo apt-get purge -qy $packageName")
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/keys/ProvisionKeys.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/keys/ProvisionKeys.kt
new file mode 100644
index 0000000..7b4bf0d
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/keys/ProvisionKeys.kt
@@ -0,0 +1,33 @@
+package org.domaindrivenarchitecture.provs.ubuntu.keys
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.core.Secret
+import org.domaindrivenarchitecture.provs.ubuntu.keys.base.configureGpgKeys
+import org.domaindrivenarchitecture.provs.ubuntu.keys.base.configureSshKeys
+import org.domaindrivenarchitecture.provs.ubuntu.secret.SecretSourceType
+import kotlinx.serialization.Serializable
+
+
+open class KeyPair(val publicKey: Secret, val privateKey: Secret)
+
+
+@Serializable
+class KeyPairSource(val sourceType: SecretSourceType, val publicKey: String, val privateKey: String) {
+ fun keyPair() : KeyPair {
+ val pub = sourceType.secret(publicKey)
+ val priv = sourceType.secret(privateKey)
+ return KeyPair(pub, priv)
+ }
+}
+
+
+/**
+ * provisions gpg and/or ssh keys for the current user
+ */
+fun Prov.provisionKeysCurrentUser(gpgKeys: KeyPair? = null, sshKeys: KeyPair? = null) = requireAll {
+ gpgKeys?.let { configureGpgKeys(it, true) }
+ sshKeys?.let { configureSshKeys(it) }
+ ProvResult(true) // dummy
+}
+
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/keys/base/Gpg.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/keys/base/Gpg.kt
new file mode 100644
index 0000000..701ffd8
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/keys/base/Gpg.kt
@@ -0,0 +1,74 @@
+package org.domaindrivenarchitecture.provs.ubuntu.keys.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createDir
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createFile
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createSecretFile
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.dirExists
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+import org.domaindrivenarchitecture.provs.ubuntu.keys.KeyPair
+import org.domaindrivenarchitecture.provs.ubuntu.utils.printToShell
+
+
+/**
+ * Installs a gpg key pair for the current user.
+ *
+ * @param gpgKeys
+ * @param trust whether to trust keys with trust-level 5 (ultimate)
+ */
+fun Prov.configureGpgKeys(gpgKeys: KeyPair, trust: Boolean = false, skipIfExistin: Boolean = true) = requireAll {
+ aptInstall("gnupg")
+ val fingerprint = gpgFingerprint(gpgKeys.publicKey.plain())
+ if (fingerprint == null) {
+ ProvResult(false, err = "Fingerprint of key could not be determined")
+ } else {
+ if (gpgKeysInstalled(fingerprint) && skipIfExistin) {
+ ProvResult(true, out = "Keys were already installed")
+ } else {
+ val pubkeyFile = "~/pub-key.asc"
+ val privkeyFile = "~/priv-key.asc"
+
+ createSecretFile(pubkeyFile, gpgKeys.publicKey)
+ createSecretFile(privkeyFile, gpgKeys.privateKey)
+
+ cmd("gpg --import $pubkeyFile")
+
+ // using option --batch for older keys; see https://superuser.com/questions/1135812/gpg2-asking-for-passphrase-when-importing-secret-keys
+ cmd("gpg --batch --import $privkeyFile")
+
+ if (trust) {
+ cmd("printf \"5\\ny\\n\" | gpg --no-tty --command-fd 0 --expert --edit-key $fingerprint trust")
+ }
+
+ cmd("shred $pubkeyFile")
+ cmd("shred $privkeyFile")
+
+ configureGPGAgent()
+ }
+ }
+}
+
+
+private fun Prov.configureGPGAgent() = def {
+ if (dirExists(".gnupg")) {
+ createDir(".gnupg", "~/")
+ }
+ val content = """
+ allow-preset-passphrase
+ allow-loopback-pinentry
+ """.trimIndent()
+ createFile("~/.gnupg/gpg-agent.conf", content)
+}
+
+
+private fun Prov.gpgKeysInstalled(fingerprint: String): Boolean {
+ return cmdNoLog("gpg --list-keys $fingerprint").success
+}
+
+
+fun Prov.gpgFingerprint(pubKey: String): String? {
+ val result =
+ cmdNoLog(" " + printToShell(pubKey) + " | gpg --with-colons --import-options show-only --import --fingerprint")
+ return result.out?.let { """^fpr:*([A-Z0-9]*):$""".toRegex(RegexOption.MULTILINE).find(it)?.groupValues?.get(1) }
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/keys/base/Ssh.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/keys/base/Ssh.kt
new file mode 100644
index 0000000..ac84160
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/keys/base/Ssh.kt
@@ -0,0 +1,46 @@
+package org.domaindrivenarchitecture.provs.ubuntu.keys.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createDir
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createSecretFile
+import org.domaindrivenarchitecture.provs.ubuntu.keys.KeyPair
+
+
+/**
+ * installs ssh keys for active user
+ */
+fun Prov.configureSshKeys(sshKeys: KeyPair) = def {
+ createDir(".ssh", "~/")
+ createSecretFile("~/.ssh/id_rsa.pub", sshKeys.publicKey, "644")
+ createSecretFile("~/.ssh/id_rsa", sshKeys.privateKey, "600")
+ configureSSHClient()
+}
+
+fun Prov.configureSSHClient() = def {
+ // TODO("Not yet implemented")
+ ProvResult(true)
+}
+
+
+/**
+ * Specifies a host or Ip to be trusted
+ *
+ * ATTENTION:
+ * This method is NOT secure as a man-in-the-middle could compromise the connection.
+ * Don't use this for critical systems resp. environments
+ */
+fun Prov.trustServer(hostOrIp: String) = def {
+ cmd("ssh-keyscan $hostOrIp >> ~/.ssh/known_hosts")
+}
+
+
+/**
+ * Checks if the specified hostname or Ip is in a known_hosts file
+ *
+ * @return whether if was found
+ */
+fun Prov.isHostKnown(hostOrIp: String) : Boolean {
+ return cmdNoEval("ssh-keygen -F $hostOrIp").out?.isNotEmpty() ?: false
+}
+
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/secret/SecretSource.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/secret/SecretSource.kt
new file mode 100644
index 0000000..438cf4e
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/secret/SecretSource.kt
@@ -0,0 +1,39 @@
+package org.domaindrivenarchitecture.provs.ubuntu.secret
+
+import org.domaindrivenarchitecture.provs.core.Secret
+import org.domaindrivenarchitecture.provs.ubuntu.secret.secretSources.*
+import kotlinx.serialization.Serializable
+
+
+@Serializable
+abstract class SecretSource(protected val input: String) {
+ abstract fun secret() : Secret
+ abstract fun secretNullable() : Secret?
+}
+
+
+@Serializable
+enum class SecretSourceType() {
+
+ PLAIN, FILE, PROMPT, PASS, GOPASS;
+
+ fun secret(input: String) : Secret {
+ return when (this) {
+ PLAIN -> PlainSecretSource(input).secret()
+ FILE -> FileSecretSource(input).secret()
+ PROMPT -> PromptSecretSource().secret()
+ PASS -> PassSecretSource(input).secret()
+ GOPASS -> GopassSecretSource(input).secret()
+ }
+ }
+}
+
+
+@Serializable
+@Suppress("unused") // for use in other projects
+class SecretSupplier(private val source: SecretSourceType, val parameter: String) {
+ fun secret(): Secret {
+ return source.secret(parameter)
+ }
+}
+
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/secret/secretSources/FileSecretSource.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/secret/secretSources/FileSecretSource.kt
new file mode 100644
index 0000000..be188d6
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/secret/secretSources/FileSecretSource.kt
@@ -0,0 +1,22 @@
+package org.domaindrivenarchitecture.provs.ubuntu.secret.secretSources
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.Secret
+import org.domaindrivenarchitecture.provs.ubuntu.secret.SecretSource
+
+
+/**
+ * Retrieve secret from a file
+ */
+class FileSecretSource(fqFileName: String) : SecretSource(fqFileName) {
+
+ override fun secret(): Secret {
+ val p = Prov.newInstance(name = "FileSecretSource")
+ return p.getSecret("cat " + input) ?: throw Exception("Failed to get secret.")
+ }
+
+ override fun secretNullable(): Secret? {
+ val p = Prov.newInstance(name = "FileSecretSource")
+ return p.getSecret("cat " + input)
+ }
+}
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/secret/secretSources/GopassSecretSource.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/secret/secretSources/GopassSecretSource.kt
new file mode 100644
index 0000000..e05fb52
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/secret/secretSources/GopassSecretSource.kt
@@ -0,0 +1,19 @@
+package org.domaindrivenarchitecture.provs.ubuntu.secret.secretSources
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.Secret
+import org.domaindrivenarchitecture.provs.ubuntu.secret.SecretSource
+
+
+/**
+ * Retrieve secret from gopass
+ */
+class GopassSecretSource(path: String) : SecretSource(path) {
+ override fun secret(): Secret {
+ return secretNullable() ?: throw Exception("Failed to get \"$input\" secret from gopass.")
+ }
+ override fun secretNullable(): Secret? {
+ val p = Prov.newInstance(name = "GopassSecretSource for $input")
+ return p.getSecret("gopass show -f " + input)
+ }
+}
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/secret/secretSources/PassSecretSource.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/secret/secretSources/PassSecretSource.kt
new file mode 100644
index 0000000..ac537de
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/secret/secretSources/PassSecretSource.kt
@@ -0,0 +1,20 @@
+package org.domaindrivenarchitecture.provs.ubuntu.secret.secretSources
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.Secret
+import org.domaindrivenarchitecture.provs.ubuntu.secret.SecretSource
+
+
+/**
+ * Retrieve secret from passwordstore on Unix
+ */
+class PassSecretSource(path: String) : SecretSource(path) {
+ override fun secret(): Secret {
+ val p = Prov.newInstance(name = "PassSecretSource")
+ return p.getSecret("pass " + input) ?: throw Exception("Failed to get secret.")
+ }
+ override fun secretNullable(): Secret? {
+ val p = Prov.newInstance(name = "PassSecretSource")
+ return p.getSecret("pass " + input)
+ }
+}
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/secret/secretSources/PlainSecretSource.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/secret/secretSources/PlainSecretSource.kt
new file mode 100644
index 0000000..cf6f795
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/secret/secretSources/PlainSecretSource.kt
@@ -0,0 +1,14 @@
+package org.domaindrivenarchitecture.provs.ubuntu.secret.secretSources
+
+import org.domaindrivenarchitecture.provs.core.Secret
+import org.domaindrivenarchitecture.provs.ubuntu.secret.SecretSource
+
+
+class PlainSecretSource(plainSecret: String) : SecretSource(plainSecret) {
+ override fun secret(): Secret {
+ return Secret(input)
+ }
+ override fun secretNullable(): Secret {
+ return Secret(input)
+ }
+}
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/secret/secretSources/PromptSecretSource.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/secret/secretSources/PromptSecretSource.kt
new file mode 100644
index 0000000..a2ab4cd
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/secret/secretSources/PromptSecretSource.kt
@@ -0,0 +1,67 @@
+package org.domaindrivenarchitecture.provs.ubuntu.secret.secretSources
+
+import org.domaindrivenarchitecture.provs.core.Secret
+import org.domaindrivenarchitecture.provs.ubuntu.secret.SecretSource
+import java.awt.FlowLayout
+import javax.swing.*
+
+
+class PasswordPanel : JPanel(FlowLayout()) {
+
+ private val passwordField = JPasswordField(20)
+ private var entered = false
+
+ val enteredPassword
+ get() = if (entered) String(passwordField.password) else null
+
+ init {
+ add(JLabel("Password: "))
+ add(passwordField)
+ passwordField.setActionCommand("OK")
+ passwordField.addActionListener {
+ if (it.actionCommand == "OK") {
+ entered = true
+
+ SwingUtilities.getWindowAncestor(it.source as JComponent)
+ .dispose()
+ }
+ }
+ }
+
+ private fun request(passwordIdentifier: String) = apply {
+ JOptionPane.showOptionDialog(null, this@PasswordPanel,
+ "Enter $passwordIdentifier",
+ JOptionPane.DEFAULT_OPTION,
+ JOptionPane.INFORMATION_MESSAGE,
+ null, emptyArray(), null)
+ }
+
+ companion object {
+
+ fun requestPassword(passwordIdentifier: String) = PasswordPanel()
+ .request(passwordIdentifier)
+ .enteredPassword
+ }
+}
+
+class PromptSecretSource(text: String = "Secret/Password") : SecretSource(text) {
+
+ override fun secret(): Secret {
+ val password = PasswordPanel.requestPassword(input)
+ if (password == null) {
+ throw IllegalArgumentException("Failed to retrieve secret from prompting.")
+ } else {
+ return Secret(password)
+ }
+ }
+
+ override fun secretNullable(): Secret? {
+ val password = PasswordPanel.requestPassword(input)
+
+ return if(password == null) {
+ null
+ }else {
+ Secret(password)
+ }
+ }
+}
\ No newline at end of file
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/user/UserConfig.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/user/UserConfig.kt
new file mode 100644
index 0000000..746dc46
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/user/UserConfig.kt
@@ -0,0 +1,29 @@
+package org.domaindrivenarchitecture.provs.ubuntu.user
+
+import org.domaindrivenarchitecture.provs.ubuntu.keys.KeyPairSource
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import java.io.BufferedReader
+import java.io.FileReader
+import java.io.FileWriter
+
+
+@Serializable
+class UserConfig(val userName: String, val gitEmail: String? = null, val gpg: KeyPairSource? = null, val ssh: KeyPairSource? = null)
+
+
+// -------------------------------------------- file methods ------------------------------------
+@Suppress("unused")
+fun readUserConfigFromFile(filename: String = "UserConfig.json") : UserConfig {
+ // read from file
+ val inputAsString = BufferedReader(FileReader(filename)).use { it.readText() }
+
+ // serializing objects
+ return Json.decodeFromString(UserConfig.serializer(), inputAsString)
+}
+
+fun writeUserConfigToFile(config: UserConfig) {
+ val fileName = "UserConfig.json"
+
+ FileWriter(fileName).use { it.write(Json.encodeToString(UserConfig.serializer(), config)) }
+}
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/user/base/User.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/user/base/User.kt
new file mode 100644
index 0000000..4d80dc3
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/user/base/User.kt
@@ -0,0 +1,166 @@
+package org.domaindrivenarchitecture.provs.ubuntu.user.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.core.Secret
+import org.domaindrivenarchitecture.provs.core.processors.RemoteProcessor
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createDirs
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.fileExists
+import org.domaindrivenarchitecture.provs.ubuntu.git.provisionGit
+import org.domaindrivenarchitecture.provs.ubuntu.keys.base.gpgFingerprint
+import org.domaindrivenarchitecture.provs.ubuntu.keys.provisionKeysCurrentUser
+import org.domaindrivenarchitecture.provs.ubuntu.user.UserConfig
+import java.net.InetAddress
+
+
+fun Prov.userExists(userName: String): Boolean {
+ return cmdNoEval("grep -c '^$userName:' /etc/passwd").success
+}
+
+
+/**
+ * Creates a new user.
+ */
+fun Prov.createUser(
+ userName: String,
+ password: Secret? = null,
+ sudo: Boolean = false,
+ copyAuthorizedKeysFromCurrentUser: Boolean = false
+): ProvResult = requireAll {
+ if (!userExists(userName)) {
+ cmd("sudo adduser --gecos \"First Last,RoomNumber,WorkPhone,HomePhone\" --disabled-password --home /home/$userName $userName")
+ }
+ password?.let { cmdNoLog("sudo echo \"$userName:${password.plain()}\" | sudo chpasswd") } ?: ProvResult(true)
+ if (sudo) {
+ makeUserSudoerWithNoSudoPasswordRequired(userName)
+ }
+ val authorizedKeysFile = "~/.ssh/authorized_keys"
+ if (copyAuthorizedKeysFromCurrentUser && fileExists(authorizedKeysFile)) {
+ createDirs("/home/$userName/.ssh")
+ val newAuthorizedKeysFile = "/home/$userName/.ssh/authorized_keys"
+ cmd("sudo cp $authorizedKeysFile $newAuthorizedKeysFile")
+ cmd("chown $userName $newAuthorizedKeysFile")
+
+ }
+ ProvResult(true) // dummy
+}
+
+
+/**
+ * Configures gpg and ssh keys for the current if keys are provided in the config.
+ * Installs and configures git for the user if gitEmail is provided in the config.
+ * Does NOT CREATE the user.
+ */
+fun Prov.configureUser(config: UserConfig) = requireAll {
+ provisionKeysCurrentUser(
+ config.gpg?.keyPair(),
+ config.ssh?.keyPair()
+ )
+
+ config.gitEmail?.run {
+ provisionGit(
+ config.userName,
+ config.gitEmail,
+ config.gpg?.keyPair()?.let { gpgFingerprint(it.publicKey.plain()) })
+ } ?: ProvResult(true)
+}
+
+
+@Suppress("unused")
+// todo create test
+fun Prov.deleteUser(userName: String, deleteHomeDir: Boolean = false): ProvResult = requireAll {
+ val flagToDeleteHomeDir = if (deleteHomeDir) " -r " else ""
+ if (userExists(userName)) {
+ cmd("sudo userdel $flagToDeleteHomeDir $userName")
+ } else {
+ ProvResult(false, err = "User $userName cannot be deleted as it does not exist.")
+ }
+}
+
+
+/**
+ * Makes userName a sudoer who does not need a password to sudo.
+ * The current (executing) user must already be a sudoer. If he is a sudoer with password required then
+ * his password must be provided.
+ */
+fun Prov.makeUserSudoerWithNoSudoPasswordRequired(
+ userName: String,
+ password: Secret? = null,
+ overwriteFile: Boolean = false
+): ProvResult = def {
+ val userSudoFile = "/etc/sudoers.d/$userName"
+ if (!fileExists(userSudoFile) || overwriteFile) {
+ val sudoPrefix = if (password == null) "sudo" else "echo ${password.plain()} | sudo -S"
+ // see https://stackoverflow.com/questions/323957/how-do-i-edit-etc-sudoers-from-a-script
+ val result = cmdNoLog(sudoPrefix + " sh -c \"echo '$userName ALL=(ALL) NOPASSWD:ALL' | (sudo su -c 'EDITOR=\"tee\" visudo -f " + userSudoFile + "')\"")
+ // don't log the command (containing the password) resp. don't include it in the ProvResult, just include success and err
+ ProvResult(result.success, err = result.err)
+ } else {
+ ProvResult(true, out = "File already exists")
+ }
+}
+
+
+/**
+ * Makes the current (executing) user be able to sudo without password.
+ * IMPORTANT: Current user must already by sudoer when calling this function.
+ */
+@Suppress("unused") // used externally
+fun Prov.makeUserSudoerWithNoSudoPasswordRequired(password: Secret) = def {
+ val currentUser = whoami()
+ if (currentUser != null) {
+ makeUserSudoerWithNoSudoPasswordRequired(currentUser, password, overwriteFile = true)
+ } else {
+ ProvResult(false, "Current user could not be determined.")
+ }
+}
+
+
+/**
+ * Checks if user is in group sudo.
+ */
+@Suppress("unused")
+fun Prov.userIsInGroupSudo(userName: String): Boolean {
+ return cmd("getent group sudo | grep -c '$userName'").success
+}
+
+
+/**
+ * Checks if current user can execute sudo commands.
+ */
+@Suppress("unused")
+fun Prov.currentUserCanSudo(): Boolean {
+ return cmd("timeout 1 sudo id").success
+}
+
+
+/**
+ * Returns username of current user if it can be determined
+ */
+fun Prov.whoami(): String? {
+ return cmd("whoami").run { if (success) out?.trim() else null }
+}
+
+
+/**
+ * Creates a new user on the specified host.
+ *
+ * @host hostname or ip-address
+ * @hostUser user on the remote system, which is used to create the new user,
+ * hostUser must be sudoer
+ * @hostPassword pw of hostUser on the remote system;
+ * ssh-key authentication will be used if hostPassword is null
+ */
+@Suppress("api") // use externally
+fun createRemoteUser(
+ host: InetAddress,
+ hostUser: String,
+ hostPassword: Secret?,
+ newUserName: String,
+ newUserPW: Secret,
+ makeNewUserSudoer: Boolean = false
+) {
+ Prov.newInstance(RemoteProcessor(host, hostUser, hostPassword), name = "createRemoteUser")
+ .createUser(newUserName, newUserPW, makeNewUserSudoer)
+}
+
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/utils/Utils.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/utils/Utils.kt
new file mode 100644
index 0000000..bad65f7
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/utils/Utils.kt
@@ -0,0 +1,11 @@
+package org.domaindrivenarchitecture.provs.ubuntu.utils
+
+import org.domaindrivenarchitecture.provs.core.escapeBackslash
+import org.domaindrivenarchitecture.provs.core.escapeDoubleQuote
+
+
+// todo: investigate to use .escapeAndEncloseByDoubleQuoteForShell() or similar instead (?)
+internal fun printToShell(text: String): String {
+ return "echo -n \"${text.escapeBackslash().escapeDoubleQuote()}\""
+}
+
diff --git a/bin/main/org/domaindrivenarchitecture/provs/ubuntu/web/base/Web.kt b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/web/base/Web.kt
new file mode 100644
index 0000000..5336d9d
--- /dev/null
+++ b/bin/main/org/domaindrivenarchitecture/provs/ubuntu/web/base/Web.kt
@@ -0,0 +1,26 @@
+package org.domaindrivenarchitecture.provs.ubuntu.web.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.isPackageInstalled
+
+
+/**
+ * Downloads a file from the given URL using curl
+ *
+ * @param path where to download to
+ * @param url file to download
+ * @param filename filename after download
+ */
+@Suppress("unused") // used externally
+fun Prov.downloadFromURL(url: String, filename: String? = null, path: String? = null, sudo: Boolean = false) : ProvResult = def {
+
+ if (!isPackageInstalled("curl")) aptInstall("curl")
+
+ if (filename == null) {
+ cmd("curl $url", path, sudo)
+ } else {
+ cmd("curl $url -o $filename", path, sudo)
+ }
+}
\ No newline at end of file
diff --git a/bin/test/WorkplaceConfigExample.json b/bin/test/WorkplaceConfigExample.json
new file mode 100644
index 0000000..5ea87e4
--- /dev/null
+++ b/bin/test/WorkplaceConfigExample.json
@@ -0,0 +1,7 @@
+{
+ "type": "MINIMAL",
+ "ssh": null,
+ "gpg": null,
+ "gitUserName": "mygitusername",
+ "gitEmail": "my@git.email"
+}
\ No newline at end of file
diff --git a/bin/test/org/domaindrivenarchitecture/provs/core/ProvTest.kt b/bin/test/org/domaindrivenarchitecture/provs/core/ProvTest.kt
new file mode 100644
index 0000000..365cf31
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/core/ProvTest.kt
@@ -0,0 +1,478 @@
+package org.domaindrivenarchitecture.provs.core
+
+import org.domaindrivenarchitecture.provs.core.docker.provideContainer
+import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
+import org.domaindrivenarchitecture.provs.test.tags.NonCi
+import org.domaindrivenarchitecture.provs.test.testLocal
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.condition.EnabledOnOs
+import org.junit.jupiter.api.condition.OS
+import java.io.ByteArrayOutputStream
+import java.io.PrintStream
+
+
+internal class ProvTest {
+
+ private fun Prov.def_returnungFalse() = def {
+ ProvResult(false)
+ }
+
+ private fun Prov.def_returningTrue() = def {
+ ProvResult(true)
+ }
+
+
+ @Test
+ @EnabledOnOs(OS.LINUX)
+ fun cmd_onLinux() {
+ // when
+ val res = Prov.newInstance(name = "testing").cmd("echo --testing--").success
+
+ // then
+ assert(res)
+ }
+
+ @Test
+ @EnabledOnOs(OS.LINUX)
+ @ContainerTest
+ fun sh_onLinux() {
+ // given
+ val script = """
+ # test some script commands
+
+ ping -c1 nu.nl
+ echo something
+ ping -c1 github.com
+ """
+
+ // when
+ val res = Prov.newInstance(name = "testing").sh(script).success
+
+ // then
+ assert(res)
+ }
+
+ @Test
+ @EnabledOnOs(OS.LINUX)
+ @ContainerTest
+ @NonCi
+ fun sh_onLinux_with_dir_and_sudo() {
+ // given
+ val script = """
+ # test some script commands
+
+ ping -c1 google.com
+ echo something
+ ping -c1 github.com
+ echo 1 # comment behind command
+ """
+
+ // when
+ val res = Prov.newInstance(name = "provs_test").sh(script, "/root", true).success
+
+ // then
+ assert(res)
+ }
+
+
+ @Test
+ @EnabledOnOs(OS.WINDOWS)
+ fun cmd_onWindows() {
+ // when
+ val res = Prov.newInstance(name = "testing").cmd("echo --testing--").success
+
+ // then
+ assert(res)
+ }
+
+ @Test
+ @EnabledOnOs(OS.WINDOWS)
+ fun sh_onWindows() {
+ // given
+ val script = """
+ # test some script commands
+
+ ping -n 1 nu.nl
+ echo something
+ ping -n 1 github.com
+ """
+
+ // when
+ val res = Prov.newInstance(name = "testing").sh(script).success
+
+ // then
+ assert(res)
+ }
+
+ @Test
+ fun def_modeOptional_result_true() {
+ // given
+ fun Prov.tst_def() = optional {
+ def_returnungFalse()
+ def_returningTrue()
+ def_returnungFalse()
+ }
+
+ // when
+ val res = testLocal().tst_def().success
+
+ // then
+ assert(res)
+ }
+
+ @Test
+ fun def_modeLast_result_true() {
+ // given
+ fun Prov.tst_def() = requireLast {
+ def_returnungFalse()
+ def_returningTrue()
+ }
+
+ // when
+ val res = testLocal().tst_def().success
+
+ // then
+ assert(res)
+ }
+
+ @Test
+ fun def_modeLast_result_false() {
+ // given
+ fun Prov.tst_def() = requireLast {
+ def_returningTrue()
+ def_returnungFalse()
+ }
+
+ // when
+ val res = testLocal().tst_def().success
+
+ // then
+ assert(!res)
+ }
+
+ @Test
+ fun def_mode_ALL_result_true() {
+ // given
+ fun Prov.tst_def_all_true_mode_ALL() = requireAll {
+ def_returningTrue()
+ def_returningTrue()
+ }
+
+ // when
+ val res = testLocal().tst_def_all_true_mode_ALL().success
+
+ // then
+ assert(res)
+ }
+
+ // given
+ fun Prov.tst_def_one_false_mode_ALL() = requireAll {
+ def_returningTrue()
+ def_returnungFalse()
+ def_returningTrue()
+ }
+
+ @Test
+ fun def_modeALL_resultFalse() {
+ // when
+ val res = testLocal().tst_def_one_false_mode_ALL().success
+
+ // then
+ assert(!res)
+ }
+
+ // given
+ fun Prov.tst_def_one_false_mode_ALL_nested() = requireAll {
+ def_returningTrue()
+ tst_def_one_false_mode_ALL()
+ def_returningTrue()
+ tst_ALL_returningTrue()
+ }
+
+ // given
+ fun Prov.tst_ALL_returningTrue() = requireAll {
+ ProvResult(true)
+ }
+
+ @Test
+ fun def_modeALLnested_resultFalse() {
+ // when
+ val res = testLocal().tst_def_one_false_mode_ALL_nested().success
+
+ // then
+ assert(!res)
+ }
+
+ @Test
+ fun def_mode_ALL_LAST_NONE_nested() {
+ // given
+ fun Prov.tst_def_last() = def {
+ def_returningTrue()
+ def_returnungFalse()
+ }
+
+ fun Prov.tst_def_one_false_mode_ALL() = requireAll {
+ tst_def_last()
+ def_returningTrue()
+ }
+
+ // when
+ val res = testLocal().tst_def_one_false_mode_ALL().success
+
+ // then
+ assert(!res)
+ }
+
+ @Test
+ fun def_mode_FAILEXIT_nested_false() {
+ // given
+ fun Prov.tst_def_failexit_inner() = exitOnFailure {
+ def_returningTrue()
+ def_returnungFalse()
+ }
+
+ fun Prov.tst_def_failexit_outer() = exitOnFailure {
+ tst_def_failexit_inner()
+ def_returningTrue()
+ }
+
+ // when
+ val res = testLocal().tst_def_failexit_outer().success
+
+ // then
+ assert(!res)
+ }
+
+ @Test
+ fun def_mode_FAILEXIT_nested_true() {
+ // given
+ fun Prov.tst_def_failexit_inner() = exitOnFailure {
+ def_returningTrue()
+ def_returningTrue()
+ }
+
+ fun Prov.tst_def_failexit_outer() = exitOnFailure {
+ tst_def_failexit_inner()
+ def_returningTrue()
+ }
+
+ // when
+ val res = testLocal().tst_def_failexit_outer().success
+
+ // then
+ assert(res)
+ }
+
+ @Test
+ fun def_mode_multiple_nested() {
+ // given
+ fun Prov.tst_nested() = def {
+ requireAll {
+ def_returningTrue()
+ def {
+ def_returnungFalse()
+ def_returningTrue()
+ }
+ def_returnungFalse()
+ def_returningTrue()
+ optional {
+ def_returnungFalse()
+ }
+ }
+ }
+
+ // when
+ val res = testLocal().tst_nested().success
+
+ // then
+ assert(!res)
+ }
+
+
+ // given
+ fun Prov.checkPrereq_evaluateToFailure() = requireLast {
+ ProvResult(false, err = "This is a test error.")
+ }
+
+ fun Prov.methodThatProvidesSomeOutput() = requireLast {
+
+ if (!checkPrereq_evaluateToFailure().success) {
+ sh(
+ """
+ echo -Start test-
+ echo Some output
+ """
+ )
+ }
+
+ sh("echo -End test-")
+ }
+
+ @Test
+ fun runProv_printsCorrectOutput() {
+
+ // given
+ val outContent = ByteArrayOutputStream()
+ val errContent = ByteArrayOutputStream()
+ val originalOut = System.out
+ val originalErr = System.err
+
+ System.setOut(PrintStream(outContent))
+ System.setErr(PrintStream(errContent))
+
+ // when
+ Prov.newInstance(name = "test instance", progressType = ProgressType.NONE).methodThatProvidesSomeOutput()
+
+ // then
+ System.setOut(originalOut)
+ System.setErr(originalErr)
+
+ println(outContent.toString())
+
+ val expectedOutput = if (OS.WINDOWS.isCurrentOs) "\n" +
+ "============================================== SUMMARY (test Instance) ============================================== \n" +
+ "> Success -- methodThatProvidesSomeOutput (requireLast) \n" +
+ "---> FAILED -- checkPrereq_evaluateToFailure (requireLast) -- Error: This is a test error.\n" +
+ "---> Success -- sh \n" +
+ "------> Success -- cmd [cmd.exe, /c, echo -Start test-]\n" +
+ "------> Success -- cmd [cmd.exe, /c, echo Some output]\n" +
+ "---> Success -- sh \n" +
+ "------> Success -- cmd [cmd.exe, /c, echo -End test-]\n" +
+ "============================================ SUMMARY END ============================================ \n"
+ else if (OS.LINUX.isCurrentOs()) {
+ "============================================== SUMMARY (test instance) ============================================== \n" +
+ "> \u001B[92mSuccess\u001B[0m -- methodThatProvidesSomeOutput (requireLast) \n" +
+ "---> \u001B[91mFAILED\u001B[0m -- checkPrereq_evaluateToFailure (requireLast) -- Error: This is a test error.\n" +
+ "---> \u001B[92mSuccess\u001B[0m -- sh \n" +
+ "------> \u001B[92mSuccess\u001B[0m -- cmd [/bin/bash, -c, echo -Start test-]\n" +
+ "------> \u001B[92mSuccess\u001B[0m -- cmd [/bin/bash, -c, echo Some output]\n" +
+ "---> \u001B[92mSuccess\u001B[0m -- sh \n" +
+ "------> \u001B[92mSuccess\u001B[0m -- cmd [/bin/bash, -c, echo -End test-]\n" +
+ "----------------------------------------------------------------------------------------------------- \n" +
+ "Overall > \u001B[92mSuccess\u001B[0m\n" +
+ "============================================ SUMMARY END ============================================ \n" +
+ "\n"
+ } else {
+ "OS " + System.getProperty("os.name") + " not yet supported"
+ }
+
+ assertEquals(expectedOutput, outContent.toString().replace("\r", ""))
+ }
+
+ @Test
+ fun check_returnsTrue() {
+ // when
+ val res = testLocal().chk("echo 123")
+
+ // then
+ assertTrue(res)
+ }
+
+ @Test
+ fun check_returnsFalse() {
+ // when
+ val res = testLocal().chk("cmddoesnotexist")
+
+ // then
+ assertFalse(res)
+ }
+
+ @Test
+ fun getSecret_returnsSecret() {
+ // when
+ val res = testLocal().getSecret("echo 123")
+
+ // then
+ assertEquals("123", res?.plain()?.trim())
+ }
+
+ @Test
+ fun addResultToEval_success() {
+ // given
+ fun Prov.inner() {
+ addResultToEval(ProvResult(true))
+ }
+
+ fun Prov.outer() = requireAll {
+ inner()
+ ProvResult(true)
+ }
+
+ // when
+ val res = testLocal().outer()
+
+ //then
+ assertEquals(ProvResult(true), res)
+ }
+
+ @Test
+ fun addResultToEval_failure() {
+ // given
+ fun Prov.inner() {
+ addResultToEval(ProvResult(false))
+ }
+
+ fun Prov.outer() = requireAll {
+ inner()
+ ProvResult(true)
+ }
+
+ // when
+ val res = testLocal().outer()
+
+ //then
+ assertEquals(ProvResult(false), res)
+ }
+
+ @Test
+ @EnabledOnOs(OS.LINUX)
+ @NonCi
+ fun inContainer_locally() {
+ // given
+ val containerName = "provs_test"
+ testLocal().provideContainer(containerName, "ubuntu")
+
+ fun Prov.inner() = def {
+ cmd("echo in container")
+ }
+
+ // then
+ fun Prov.outer() = def {
+ inContainer(containerName) {
+ inner()
+ cmd("echo testfile > testfile.txt")
+ }
+ }
+
+ val res = testLocal().def { outer() }
+
+ // then
+ assertEquals(true, res.success)
+ }
+
+ @Test
+ @EnabledOnOs(OS.LINUX)
+ @Disabled // run manually after updating host and remoteUser
+ fun inContainer_remotely() {
+ // given
+ val host = "192.168.56.135"
+ val remoteUser = "az"
+
+ fun Prov.inner() = def {
+ cmd("echo 'in testfile' > testfile.txt")
+ }
+
+ // then
+ val res = remote(host, remoteUser).def {
+ inner() // executed on the remote host
+ inContainer("prov_default") {
+ inner() // executed in the container on the remote host
+ }
+ }
+
+ // then
+ assertEquals(true, res.success)
+ }
+}
diff --git a/bin/test/org/domaindrivenarchitecture/provs/core/UtilsKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/core/UtilsKtTest.kt
new file mode 100644
index 0000000..a0f7865
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/core/UtilsKtTest.kt
@@ -0,0 +1,49 @@
+package org.domaindrivenarchitecture.provs.core
+
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.Test
+import java.net.UnknownHostException
+
+internal class UtilsKtTest {
+
+ @Test
+ fun test_getCallingMethodName() {
+ // when
+ val s = getCallingMethodName()
+
+ // then
+ assertEquals("test_getCallingMethodName", s)
+ }
+
+ @Test
+ @ContainerTest
+ fun runCmdInContainer() {
+ // when
+ val res = defaultTestContainer().cmd("echo something")
+
+ // then
+ assertTrue(res.success)
+ }
+
+ @Test
+ fun remote_emptyHost() {
+ assertThrows(IllegalArgumentException::class.java,
+ { remote("", "user") })
+ }
+
+ @Test
+ fun remote_invalidHost() {
+ assertThrows(
+ UnknownHostException::class.java,
+ { remote("invalid_host", "user") })
+ }
+
+ @Test
+ @Disabled // run manually after having updated user
+ fun test_remote() {
+ assertTrue(remote("127.0.0.1", "user").cmd("echo sth").success)
+ }
+}
\ No newline at end of file
diff --git a/bin/test/org/domaindrivenarchitecture/provs/core/docker/platforms/UbuntuHostDockerKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/core/docker/platforms/UbuntuHostDockerKtTest.kt
new file mode 100644
index 0000000..c5bb42b
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/core/docker/platforms/UbuntuHostDockerKtTest.kt
@@ -0,0 +1,32 @@
+package org.domaindrivenarchitecture.provs.core.docker.platforms
+
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.core.docker.containerRuns
+import org.domaindrivenarchitecture.provs.core.docker.exitAndRmContainer
+import org.domaindrivenarchitecture.provs.core.docker.runContainer
+import org.domaindrivenarchitecture.provs.test.tags.NonCi
+import org.domaindrivenarchitecture.provs.test.testLocal
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.condition.EnabledOnOs
+import org.junit.jupiter.api.condition.OS
+
+internal class UbuntuHostDockerKtTest {
+
+ @Test
+ @EnabledOnOs(OS.LINUX)
+ @NonCi
+ fun runAndCheckAndExitContainer() {
+ // when
+ val containerName = "testContainer"
+ val result = testLocal().requireAll {
+ runContainer(containerName)
+ addResultToEval(ProvResult(containerRuns(containerName)))
+
+ exitAndRmContainer(containerName)
+ }
+
+ // then
+ assertEquals(ProvResult(true), result)
+ }
+}
\ No newline at end of file
diff --git a/bin/test/org/domaindrivenarchitecture/provs/core/entry/EntryTest.kt b/bin/test/org/domaindrivenarchitecture/provs/core/entry/EntryTest.kt
new file mode 100644
index 0000000..fca7e30
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/core/entry/EntryTest.kt
@@ -0,0 +1,85 @@
+package org.domaindrivenarchitecture.provs.core.entry
+
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import java.io.ByteArrayOutputStream
+import java.io.PrintStream
+
+
+@Suppress("unused")
+fun testfun(args: Array) {
+ println("test is fun " + args.joinToString(" "))
+}
+
+@Suppress("unused")
+fun main(args: Array) {
+ println("main is fun " + args.joinToString(" "))
+}
+
+
+internal class EntryKtTest {
+
+ private var outContent = ByteArrayOutputStream()
+ private var originalOut = System.out
+
+ @BeforeEach
+ fun redirectSystemOutStream() {
+ originalOut = System.out
+
+ // given
+ outContent = ByteArrayOutputStream()
+ System.setOut(PrintStream(outContent))
+ }
+
+ @AfterEach
+ fun restoreSystemOutStream() {
+ System.setOut(originalOut)
+ }
+
+ @Test
+ fun test_without_method_argument() {
+ // when
+ main("org.domaindrivenarchitecture.provs.core.entry.EntryTestKt")
+
+ // then
+ assertEquals("main is fun \n", outContent.toString())
+ }
+
+ @Test
+ fun test_method_main_without_args() {
+ // when
+ main("org.domaindrivenarchitecture.provs.core.entry.EntryTestKt", "main")
+
+ // then
+ assertEquals("main is fun \n", outContent.toString())
+ }
+
+ @Test
+ fun test_named_method_without_args() {
+ // when
+ main("org.domaindrivenarchitecture.provs.core.entry.EntryTestKt", "testfun")
+
+ // then
+ assertEquals("test is fun \n", outContent.toString())
+ }
+
+ @Test
+ fun test_method_main_with_args() {
+ // when
+ main("org.domaindrivenarchitecture.provs.core.entry.EntryTestKt", "main", "arg1", "arg2")
+
+ // then
+ assertEquals("main is fun arg1 arg2\n", outContent.toString())
+ }
+
+ @Test
+ fun test_named_method_with_args() {
+ // when
+ main("org.domaindrivenarchitecture.provs.core.entry.EntryTestKt", "testfun", "arg1", "arg2")
+
+ // then
+ assertEquals("test is fun arg1 arg2\n", outContent.toString())
+ }
+}
diff --git a/bin/test/org/domaindrivenarchitecture/provs/core/platformTest/UbuntuProvTests.kt b/bin/test/org/domaindrivenarchitecture/provs/core/platformTest/UbuntuProvTests.kt
new file mode 100644
index 0000000..b7f5bf0
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/core/platformTest/UbuntuProvTests.kt
@@ -0,0 +1,99 @@
+package org.domaindrivenarchitecture.provs.core.platformTest
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.test.tags.NonCi
+import org.domaindrivenarchitecture.provs.test.testLocal
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.condition.EnabledOnOs
+import org.junit.jupiter.api.condition.OS
+
+internal class UbuntuProvTests {
+
+ private fun Prov.ping(url: String) = def {
+ xec("ping", "-c", "4", url)
+ }
+
+ private fun Prov.outerPing() = def {
+ ping("gitlab.com")
+ }
+
+ @Test
+ @EnabledOnOs(OS.LINUX)
+ fun that_ping_works() {
+ // when
+ val res = testLocal().outerPing()
+
+ // then
+ assert(res.success)
+ }
+
+ @Test
+ @EnabledOnOs(OS.LINUX)
+ fun that_cmd_works() {
+ // given
+ val a = testLocal()
+
+ // when
+ val res1 = a.cmd("pwd")
+ val dir = res1.out?.trim()
+ val res2 = a.cmd("echo abc", dir)
+
+ // then
+ assert(res1.success)
+ assert(res2.success)
+ assert(res2.out?.trim() == "abc")
+ }
+
+ @Test
+ @EnabledOnOs(OS.LINUX)
+ @NonCi
+ fun that_cmd_works_with_sudo() {
+ // given
+ val a = testLocal()
+
+ // when
+ val res1 = a.cmd("echo abc", "/root", sudo = true)
+
+ // then
+ assert(res1.success)
+ assert(res1.out?.trim() == "abc")
+ }
+
+ @Test
+ @EnabledOnOs(OS.LINUX)
+ fun that_nested_shells_work() {
+ // given
+ val a = testLocal()
+
+ // when
+ val res1 = a.cmd("pwd")
+ val dir = res1.out?.trim()
+ val res2 = a.cmd("echo abc", dir)
+
+ // then
+ assert(res1.success)
+ assert(res2.success)
+ assert(res2.out?.trim() == "abc")
+ }
+
+ @Test
+ @EnabledOnOs(OS.LINUX)
+ fun that_xec_works() {
+ // given
+ val a = testLocal()
+
+ // when
+ val res1 = a.xec("/usr/bin/printf", "hi")
+ val res2 = a.xec("/bin/ping", "-c", "2", "gitlab.com")
+ val res3 = a.xec("/bin/bash", "-c", "echo echoed")
+
+ // then
+ assert(res1.success)
+ assert(res1.out?.trim() == "hi")
+ assert(res2.success)
+ assert(res3.success)
+ assert(res3.out?.trim() == "echoed")
+ }
+
+}
+
diff --git a/bin/test/org/domaindrivenarchitecture/provs/core/platformTest/WinProvTests.kt b/bin/test/org/domaindrivenarchitecture/provs/core/platformTest/WinProvTests.kt
new file mode 100644
index 0000000..9ed2d6d
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/core/platformTest/WinProvTests.kt
@@ -0,0 +1,45 @@
+package org.domaindrivenarchitecture.provs.core.platformTest
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.test.testLocal
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.condition.EnabledOnOs
+import org.junit.jupiter.api.condition.OS
+
+internal class WinProvTests {
+
+ private fun Prov.ping(url: String) = def {
+ cmd("ping $url")
+ }
+
+ private fun Prov.outerPing() = def { ping("nu.nl") }
+
+ @Test
+ @EnabledOnOs(OS.WINDOWS)
+ fun def_definesPing_function() {
+ // when
+ val res = testLocal().outerPing()
+
+ // then
+ assert(res.success)
+ }
+
+ @Test
+ @EnabledOnOs(OS.WINDOWS)
+ fun cmd_executesCommand() {
+ // given
+ val a = testLocal()
+
+ // when
+ val res1 = a.cmd("echo %cd%")
+ val dir = res1.out?.trim()
+ val res2 = a.cmd("echo abc", dir)
+
+ // then
+ assert(res1.success)
+ assert(res1.success)
+ assertEquals( "abc", res2.out?.trim())
+ }
+}
+
diff --git a/bin/test/org/domaindrivenarchitecture/provs/core/processors/ContainerProcessorTest.kt b/bin/test/org/domaindrivenarchitecture/provs/core/processors/ContainerProcessorTest.kt
new file mode 100644
index 0000000..26fd034
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/core/processors/ContainerProcessorTest.kt
@@ -0,0 +1,47 @@
+package org.domaindrivenarchitecture.provs.core.processors
+
+import org.domaindrivenarchitecture.provs.core.newline
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.condition.EnabledOnOs
+import org.junit.jupiter.api.condition.OS
+
+@EnabledOnOs(OS.LINUX)
+internal class ContainerProcessorTest {
+
+ @Test
+ @ContainerTest
+ fun cmd_works_with_echo() {
+
+ // given
+ val prov = defaultTestContainer()
+ val text = "abc123!§$%&/#äöü"
+
+ // when
+ val res = prov.cmd("echo '${text}'")
+
+ // then
+ assert(res.success)
+ assertEquals(text + newline(), res.out)
+ }
+
+
+ @Test
+ @ContainerTest
+ fun cmdNoLog_works_with_echo() {
+ // given
+ val prov = defaultTestContainer()
+ val text = "abc123!§$%&/#äöü"
+
+ // when
+ val res = prov.cmdNoLog("echo '${text}'")
+
+ // then
+ assert(res.success)
+ assertEquals(text + newline(), res.out)
+
+ // todo add check that cmd was not logged
+ }
+}
diff --git a/bin/test/org/domaindrivenarchitecture/provs/core/processors/ContainerUbuntuHostProcessorTest.kt b/bin/test/org/domaindrivenarchitecture/provs/core/processors/ContainerUbuntuHostProcessorTest.kt
new file mode 100644
index 0000000..b055d63
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/core/processors/ContainerUbuntuHostProcessorTest.kt
@@ -0,0 +1,30 @@
+package org.domaindrivenarchitecture.provs.core.processors
+
+import org.domaindrivenarchitecture.provs.core.platforms.SHELL
+import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
+import org.domaindrivenarchitecture.provs.test.testDockerWithSudo
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.condition.EnabledOnOs
+import org.junit.jupiter.api.condition.OS.LINUX
+
+val DEFAULT_START_MODE_TEST_CONTAINER = ContainerStartMode.USE_RUNNING_ELSE_CREATE
+
+class ContainerUbuntuHostProcessorTest {
+
+ @Test
+ @EnabledOnOs(LINUX)
+ @ContainerTest
+ fun test_execution() {
+ // given
+ val processor =
+ ContainerUbuntuHostProcessor("provs_ubuntuhost_test", "ubuntu", DEFAULT_START_MODE_TEST_CONTAINER, sudo = testDockerWithSudo)
+
+ // when
+ val res = processor.x(SHELL, "-c", "echo -n abc")
+
+ // then
+ assertEquals(0, res.exitCode)
+ assertEquals("abc", res.out)
+ }
+}
\ No newline at end of file
diff --git a/bin/test/org/domaindrivenarchitecture/provs/core/processors/LocalProcessorTest.kt b/bin/test/org/domaindrivenarchitecture/provs/core/processors/LocalProcessorTest.kt
new file mode 100644
index 0000000..f6e6dc5
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/core/processors/LocalProcessorTest.kt
@@ -0,0 +1,97 @@
+package org.domaindrivenarchitecture.provs.core.processors
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.escapeAndEncloseByDoubleQuoteForShell
+import org.domaindrivenarchitecture.provs.core.escapeProcentForPrintf
+import org.domaindrivenarchitecture.provs.core.escapeSingleQuoteForShell
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.condition.EnabledOnOs
+import org.junit.jupiter.api.condition.OS
+
+
+internal class LocalProcessorTest {
+
+ @Test
+ @EnabledOnOs(OS.LINUX)
+ fun cmd_with_printf_on_Linux() {
+ // given
+ val prov = Prov.newInstance()
+ val text = "abc123!§\\\$%%&/\"\\äöü'"
+
+ // when
+ val res = prov.cmd("printf '${text.replace("%", "%%").escapeSingleQuoteForShell()}'")
+
+ // then
+ assert(res.success)
+ assert(res.out == text)
+ }
+
+
+ @Test
+ @EnabledOnOs(OS.LINUX)
+ fun cmd_with_nested_shell_and_printf_on_Linux() {
+ // given
+ val prov = Prov.newInstance()
+ val text = "abc123!§\\$%%&/\"\\äöü'"
+
+ // when
+ val res = prov.cmd("sh -c " + ("sh -c " + ("printf ${text.escapeProcentForPrintf().escapeAndEncloseByDoubleQuoteForShell()}").escapeAndEncloseByDoubleQuoteForShell()).escapeAndEncloseByDoubleQuoteForShell())
+
+ // then
+ assertTrue(res.success)
+ assertEquals(text, res.out)
+ }
+
+
+ @Test
+ @EnabledOnOs(OS.WINDOWS)
+ fun cmd_with_echo_on_Windows() {
+ // given
+ val prov = Prov.newInstance()
+ val text = "abc123!\"#"
+
+ // when
+ val res = prov.cmd("echo $text")
+
+ // then
+ assert(res.success)
+ assertEquals( text + "\r\n", res.out)
+ }
+
+
+ @Test
+ @EnabledOnOs(OS.LINUX)
+ fun cmdNoLog_linux() {
+ // given
+ val prov = Prov.newInstance()
+ val text = "abc123!#"
+ val osSpecificText = if (OS.WINDOWS.isCurrentOs) text else "'$text'"
+
+
+ // when
+ val res = prov.cmdNoLog("echo $osSpecificText")
+
+ // then
+ assert(res.success)
+ assertEquals( text + System.lineSeparator(), res.out)
+
+ // todo add check that cmd was not logged
+ }
+
+
+ @Test
+ fun cmd_forUnkownCommand_resultWithError() {
+ // given
+ val prov = Prov.newInstance()
+
+ // when
+ val res = prov.cmd("iamanunknowncmd")
+
+ // then
+ assert(!res.success)
+ assert(res.out.isNullOrEmpty())
+ assert(!res.err.isNullOrEmpty())
+ }
+}
\ No newline at end of file
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/firewall/ProvisionFirewallKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/firewall/ProvisionFirewallKtTest.kt
new file mode 100644
index 0000000..05e1327
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/firewall/ProvisionFirewallKtTest.kt
@@ -0,0 +1,46 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.firewall
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.docker.dockerProvideImage
+import org.domaindrivenarchitecture.provs.core.docker.dockerimages.UbuntuPlusUser
+import org.domaindrivenarchitecture.provs.core.docker.exitAndRmContainer
+import org.domaindrivenarchitecture.provs.core.local
+import org.domaindrivenarchitecture.provs.core.processors.ContainerEndMode
+import org.domaindrivenarchitecture.provs.core.processors.ContainerStartMode
+import org.domaindrivenarchitecture.provs.core.processors.ContainerUbuntuHostProcessor
+import org.domaindrivenarchitecture.provs.test.tags.NonCi
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+
+
+internal class ProvisionFirewallKtTest {
+
+ @Test
+ @NonCi
+ fun configureFirewall() {
+ // given
+ val dockerImage = UbuntuPlusUser()
+ local().dockerProvideImage(dockerImage)
+ val containerName = "firewall_test"
+ local().exitAndRmContainer(containerName)
+ local().cmd("sudo docker run --cap-add=NET_ADMIN -dit --name $containerName ${dockerImage.imageName()}")
+ val a = Prov.newInstance(
+ ContainerUbuntuHostProcessor(
+ containerName,
+ dockerImage.imageName(),
+ ContainerStartMode.USE_RUNNING_ELSE_CREATE, // already started in previous statement
+ ContainerEndMode.EXIT_AND_REMOVE
+ ))
+
+ // when
+ val res = a.requireAll {
+ aptInstall("iptables")
+ provisionFirewall()
+ }
+ local().exitAndRmContainer(containerName)
+
+ // then
+ assertTrue(res.success)
+ }
+}
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nexus/ProvisionNexusKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nexus/ProvisionNexusKtTest.kt
new file mode 100644
index 0000000..5ea4387
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nexus/ProvisionNexusKtTest.kt
@@ -0,0 +1,26 @@
+package nexus
+
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nexus.provisionNexusWithDocker
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.Test
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+
+internal class ProvisionNexusKtTest {
+
+ @Test
+ @Disabled("Find out how to run docker in docker")
+ fun provisionNexusWithDocker() {
+ // given
+ val a = defaultTestContainer()
+
+ // when
+ val res = a.requireAll {
+ provisionNexusWithDocker()
+ }
+
+ // then
+ assertTrue(res.success)
+ }
+}
\ No newline at end of file
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/ProvisionNginxKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/ProvisionNginxKtTest.kt
new file mode 100644
index 0000000..9e47975
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/ProvisionNginxKtTest.kt
@@ -0,0 +1,89 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx
+
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+import org.domaindrivenarchitecture.provs.test.tags.NonCi
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.replaceTextInFile
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.base.*
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.fileExists
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+
+
+internal class ProvisionNginxKtTest {
+
+ @Test
+ @NonCi
+ fun provisionNginxStandAlone_customConfig() {
+ // given
+ val a = defaultTestContainer()
+ val config = """
+ events {} # event context have to be defined to consider config valid
+
+ http {
+ server {
+ listen 80;
+ server_name localhost;
+
+ return 200 "Hello";
+ }
+ }
+ """.trimIndent()
+ a.aptInstall("curl")
+
+ // when
+ val res = a.requireAll {
+ provisionNginxStandAlone(NginxConf(config))
+ cmd("curl localhost")
+ }
+
+ // then
+ assertTrue(res.success)
+ }
+
+ @Test
+ @NonCi
+ fun provisionNginxStandAlone_defaultConfig() {
+ // given
+ val a = defaultTestContainer()
+
+ // when
+ val res = a.requireAll {
+ provisionNginxStandAlone()
+ }
+
+ // then
+ assertTrue(res.success)
+ }
+
+ @Test
+ @NonCi
+ fun provisionNginxStandAlone_sslConfig() {
+ // given
+ val a = defaultTestContainer()
+ a.def {
+ val file = "/etc/ssl/openssl.cnf"
+ if (fileExists(file)) {
+ replaceTextInFile(file, "RANDFILE", "#RANDFILE")
+ }
+ aptInstall("openssl")
+ }
+
+ // when
+ val res = a.def {
+ nginxCreateSelfSignedCertificate()
+
+ provisionNginxStandAlone(
+ NginxConf.nginxReverseProxySslConfig(
+ "localhost",
+ dirSslCert + "/" + certificateName + ".crt",
+ dirSslKey + "/" + certificateName + ".key"
+ )
+ )
+ }
+
+ // then
+ assertTrue(res.success)
+ }
+}
+
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/base/LocationsKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/base/LocationsKtTest.kt
new file mode 100644
index 0000000..a1a387d
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/server_software/nginx/base/LocationsKtTest.kt
@@ -0,0 +1,35 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.base
+
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createFile
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.fileContainsText
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.configFile
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.nginx.provisionNginxStandAlone
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+import org.domaindrivenarchitecture.provs.test.tags.NonCi
+
+internal class LocationsKtTest {
+
+ @Test
+ @NonCi
+ fun nginxIncludeLocationFolders() {
+ // given
+ val a = defaultTestContainer()
+ a.provisionNginxStandAlone()
+ a.createFile(configFile, NGINX_MINIMAL_CONF, sudo = true)
+
+ // when
+ val res = a.nginxIncludeLocationFolders()
+
+ // then
+ assertTrue(res.success)
+ assertTrue(a.fileContainsText(
+ configFile, """listen 80;
+ include /etc/nginx/locations-enabled/port80*.conf
+ include /etc/nginx/locations-enabled/port443*.conf"""))
+ // just 1 occurrence
+ assertEquals("1", a.cmd("grep -o 'listen 80;' $configFile | wc -l").out?.trim())
+ }
+}
\ No newline at end of file
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/test_keys/TestKeys.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/test_keys/TestKeys.kt
new file mode 100644
index 0000000..d85811a
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/test_keys/TestKeys.kt
@@ -0,0 +1,166 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.test_keys
+
+
+fun publicGPGSnakeoilKey(): String {
+ return """-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+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
+KpkCQg==
+=cS1b
+-----END PGP PUBLIC KEY BLOCK----- """
+}
+
+fun privateGPGSnakeoilKey(): String {
+ return """-----BEGIN PGP PRIVATE KEY BLOCK-----
+
+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
+i+XV3Dazj3nq/DxUB0neLU/r1afAEiqZAkI=
+=h5SJ
+-----END PGP PRIVATE KEY BLOCK-----""".trimIndent()
+}
+
+fun publicSSHSnakeoilKey(): String {
+ return """ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDOtQOq8a/Z7SdZVPrh+Icaq5rr+Qg1TZP4IPuRoFgfujUztQ2dy5DfTEbabJ0qHyo+PKwBDQorVohrW7CwvCEVQQh2NLuGgnukBN2ut5Lam7a/fZBoMjAyTvD4bXyEsUr/Bl5CLoBDkKM0elUxsc19ndzSofnDWeGyQjJIWlkNkVk/ybErAnIHVE+D+g3UxwA+emd7BF72RPqdVN39Eu4ntnxYzX0eepc8rkpFolVn6+Ai4CYHE4FaJ7bJ9WGPbwLuDl0pw/Cp3ps17cB+JlQfJ2spOq0tTVk+GcdGnt+mq0WaOnvVeQsGJ2O1HpY3VqQd1AsC2UOyHhAQ00pw7Pi9 snake@oil.com"""
+}
+
+fun privateSSHSnakeoilKey(): String {
+ return """
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIEowIBAAKCAQEAzrUDqvGv2e0nWVT64fiHGqua6/kINU2T+CD7kaBYH7o1M7UN
+ ncuQ30xG2mydKh8qPjysAQ0KK1aIa1uwsLwhFUEIdjS7hoJ7pATdrreS2pu2v32Q
+ aDIwMk7w+G18hLFK/wZeQi6AQ5CjNHpVMbHNfZ3c0qH5w1nhskIySFpZDZFZP8mx
+ KwJyB1RPg/oN1McAPnpnewRe9kT6nVTd/RLuJ7Z8WM19HnqXPK5KRaJVZ+vgIuAm
+ BxOBWie2yfVhj28C7g5dKcPwqd6bNe3AfiZUHydrKTqtLU1ZPhnHRp7fpqtFmjp7
+ 1XkLBidjtR6WN1akHdQLAtlDsh4QENNKcOz4vQIDAQABAoIBAGrgAsZ28gJOcSLq
+ IlGF62zpv0800n6k3tXTT98qtYWqBGn4udKVdxFNYfD7aYNm27OUMSbV9CUWN7Cy
+ lre6fax8lIBxoWfZvU2/ylLUzZREIIf/xxNop6zLTiJUkaYV+P3E8CVt35mPhiLT
+ AYuRL/s8DPnHD9lmdqBxQ4hPVm4Bg7JZxbyN8in3PP1UkdWKxg91O1LYewIZHszq
+ y9BdklKyxQ+fcYP5DD9KkULAjdab48GIxQETrZKp7zV0KiGrjF4Axf5y5yT2jmFT
+ nZ1uZrC1MJTMYyKTBR7wsSpVBMSMUsh5XtxdJo4FuP6g9Kn6AkeQ/Y1shcWVfQgw
+ 6009o8ECgYEA8J1PtnVCHxMLiVKZznzvgCe+EV0RkvuB9PGPdfpLfkHa1DKS+FzH
+ 80D+Vqe0rQNLudG5Qj53MPghNirGyrjXwTYFW9xCqq9hrzfxEI4xIYOd4gHoPMMQ
+ pfWZylP9GYQp/uoa+e/fcdXRSv1IDLRwJZ5XpMtWAIfvMOyDhbfjehECgYEA2+yp
+ poey1y6RWuaIQd2a/PKuYk9jvLEETiz6q7t63MFd6e9cUYX02cG/6yzz6piTWUtx
+ pk9e9IjclLUgV/twVz8SUgSw5TcqBrMnuIT4yQ5rQNZqiEvpCfgb5itcW7I3ADGy
+ dsz2kgaAm7QVZlndQKIy7xRYBCnCD3VQ+TiWh+0CgYAT3qnKg3xmXIhDWtLgvmh4
+ yM9lV64v2R0uQRR7xaOeVYngpByG7gKFEATw2wCMmQ0T10HZOpdVL+huNLId443N
+ osxmfZXzym/irFf36gYcomXTWBz5h5JEYjfFAZKRHNzq9CIuKaTmHaYe7zOX+P6Z
+ 3K2YKkJ74L3b6GwkCr96QQKBgQC6n0iTTSGg4h5skaXcpq2HqnP6br4G9/vcTuTk
+ Z/JpdBk6k2i2sULGqlguu/W8BH89Tf0CEOZWAfGUq2Ln5jE9iAMG4H4v9DDQgKTb
+ OtNW4cp3uburLydw0z7xgagdE80CeCmmEGXIIoZuGlHyiZ1r5HfuU0ghOEI6FeaB
+ pdhvPQKBgEpmHV66wqSzzxmYxKjUu8gl9rIniG8SWXHlvcoGVwt1qdOMtNtvwDgB
+ DnbUbANSjzIfFSqVwlx7nXG1e1yN7F1YuyUa3I5QEm4+5URoTSDghk03LTFH+kfM
+ OUxwE8Su4WnoQc7WjkTG0M3FECAu7TEcF9uqdcEsW+4+JMAhE5oo
+ -----END RSA PRIVATE KEY-----
+ """.trimIndent()
+}
\ No newline at end of file
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/ProvisionWorkplaceKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/ProvisionWorkplaceKtTest.kt
new file mode 100644
index 0000000..38f4f6e
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/ProvisionWorkplaceKtTest.kt
@@ -0,0 +1,51 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace
+
+import org.domaindrivenarchitecture.provs.core.Password
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+
+internal class ProvisionWorkplaceKtTest {
+
+ @Test
+ fun provisionWorkplace() {
+ // given
+ val a = defaultTestContainer()
+
+ // when
+ // in order to test WorkplaceType.OFFICE: fix installing libreoffice for a fresh container as it hangs the first time but succeeds 2nd time
+ val res = a.provisionWorkplace(
+ WorkplaceType.MINIMAL,
+ gitUserName = "testuser",
+ gitEmail = "testuser@test.org",
+ userPassword = Password("testuser")
+ )
+
+ // then
+ assertTrue(res.success)
+ }
+
+
+ @Test
+ fun provisionWorkplaceFromConfigFile() {
+ // given
+ val a = defaultTestContainer()
+
+ // when
+ // in order to test WorkplaceType.OFFICE: fix installing libreoffice for a fresh container as it hangs the first time but succeeds 2nd time
+ val config = readWorkplaceConfigFromFile("src/test/resources/WorkplaceConfigExample.json")
+ ?: throw Exception("Could not read WorkplaceConfig")
+ val res = a.provisionWorkplace(
+ config.type,
+ config.ssh?.keyPair(),
+ config.gpg?.keyPair(),
+ config.gitUserName,
+ config.gitEmail,
+ )
+
+ // then
+ assertTrue(res.success)
+ }
+}
+
+
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/FakturamaKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/FakturamaKtTest.kt
new file mode 100644
index 0000000..73e81fe
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/FakturamaKtTest.kt
@@ -0,0 +1,19 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace.base
+
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+
+internal class FakturamaKtTest {
+
+ @Test
+ fun installFakturama() {
+ // given
+ val a = defaultTestContainer()
+ // when
+ val res = a.def { installFakturama() }
+ // then
+ assertTrue(res.success)
+ }
+}
\ No newline at end of file
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/GopassBridgeKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/GopassBridgeKtTest.kt
new file mode 100644
index 0000000..8b8f79f
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/GopassBridgeKtTest.kt
@@ -0,0 +1,143 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.ProvResult
+import org.domaindrivenarchitecture.provs.core.Secret
+import org.domaindrivenarchitecture.provs.core.docker.exitAndRmContainer
+import org.domaindrivenarchitecture.provs.core.local
+import org.domaindrivenarchitecture.provs.core.processors.ContainerStartMode
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
+import org.domaindrivenarchitecture.provs.test.tags.NonCi
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+import org.domaindrivenarchitecture.provs.ubuntu.keys.KeyPair
+import org.domaindrivenarchitecture.provs.ubuntu.keys.base.configureGpgKeys
+import org.junit.jupiter.api.Assertions.assertFalse
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.test_keys.privateGPGSnakeoilKey
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.test_keys.publicGPGSnakeoilKey
+
+
+internal class GopassBridgeKtTest {
+
+ @ContainerTest
+ @Test
+ fun test_downloadGopassBridge() {
+ // given
+ local().exitAndRmContainer("provs_test")
+ val a = defaultTestContainer()
+ a.aptInstallCurl()
+
+ // when
+ val res = a.downloadGopassBridge()
+
+ // then
+ assertTrue(res.success)
+ }
+
+ @ContainerTest
+ @Test
+ fun test_install_and_configure_GopassBridgeJsonApi() {
+ // given
+ local().exitAndRmContainer("provs_test")
+ val a = defaultTestContainer()
+ val preparationResult = a.def {
+ aptInstallCurl()
+ configureGpgKeys(
+ KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())),
+ trust = true,
+ skipIfExistin = false
+ )
+ installGopass()
+ if (!chk("gopass ls")) {
+ // configure/init gopass in default location with gpg-key-fingerprint of snakeoil keys
+ cmd("printf \"\\ntest\\ntest@test.org\\n\" | gopass init 0x0674104CA81A4905")
+ } else {
+ ProvResult(true, out = "gopass already configured")
+ }
+ }
+ assertTrue(preparationResult.success)
+
+ // when
+ val res = a.def {
+ installGopassBridgeJsonApi()
+ configureGopassBridgeJsonApi()
+ }
+
+ // then
+ assertTrue(res.success)
+ }
+
+ @ContainerTest
+ @Test
+ @NonCi
+ fun test_install_GopassBridgeJsonApi_with_incompatible_gopass_jsonapi_version_installed() {
+ // given
+ val a = defaultTestContainer(ContainerStartMode.CREATE_NEW_KILL_EXISTING)
+ val preparationResult = a.def {
+ aptInstallCurl()
+
+ configureGpgKeys(
+ KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())),
+ trust = true,
+ skipIfExistin = false
+ )
+ installGopass("1.11.0", enforceVersion = true)
+ if (!chk("gopass ls")) {
+ // configure gopass in default location with gpg-key-fingerprint of snakeoil keys
+ cmd("printf \"\\ntest\\ntest@test.org\\n\" | gopass init 0x0674104CA81A4905")
+ } else {
+ ProvResult(true, out = "gopass already configured")
+ }
+ }
+ assertTrue(preparationResult.success)
+
+ // when
+ val res = a.def {
+ installGopassBridgeJsonApi()
+ configureGopassBridgeJsonApi()
+ }
+
+ // then
+ assertFalse(res.success)
+ }
+
+ @ContainerTest
+ @Test
+ @NonCi
+ fun test_install_GopassBridgeJsonApi_with_incompatible_gopass_version_installed() {
+ // given
+ val a = defaultTestContainer(ContainerStartMode.CREATE_NEW_KILL_EXISTING)
+ val preparationResult = a.def {
+ aptInstallCurl()
+ configureGpgKeys(
+ KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())),
+ trust = true,
+ skipIfExistin = false
+ )
+ installGopass("1.9.0", enforceVersion = true)
+ if (!chk("gopass ls")) {
+ // configure gopass in default location with gpg-key-fingerprint of snakeoil keys
+ cmd("printf \"\\ntest\\ntest@test.org\\n\" | gopass init 0x0674104CA81A4905")
+ } else {
+ ProvResult(true, out = "gopass already configured")
+ }
+ }
+ assertTrue(preparationResult.success)
+
+ // when
+ val res = a.def {
+ installGopassBridgeJsonApi()
+ configureGopassBridgeJsonApi()
+ }
+
+ // then
+ assertFalse(res.success)
+ }
+
+ private fun Prov.aptInstallCurl() = def {
+ cmd("apt-get update", sudo = true)
+ aptInstall("curl")
+ }
+}
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/GopassKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/GopassKtTest.kt
new file mode 100644
index 0000000..46a62cf
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/GopassKtTest.kt
@@ -0,0 +1,90 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace.base
+
+import org.domaindrivenarchitecture.provs.core.Secret
+import org.domaindrivenarchitecture.provs.core.remote
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.*
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+import org.domaindrivenarchitecture.provs.ubuntu.keys.KeyPair
+import org.domaindrivenarchitecture.provs.ubuntu.keys.base.configureGpgKeys
+import org.domaindrivenarchitecture.provs.ubuntu.keys.base.gpgFingerprint
+import org.domaindrivenarchitecture.provs.ubuntu.secret.secretSources.GopassSecretSource
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.Test
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.test_keys.privateGPGSnakeoilKey
+import org.domaindrivenarchitecture.provs.ubuntu.extensions.test_keys.publicGPGSnakeoilKey
+
+
+internal class GopassKtTest {
+
+ @ContainerTest
+ @Test
+ fun test_installAndConfigureGopassAndMountStore() {
+ // given
+ val a = defaultTestContainer()
+ val gopassRootDir = ".password-store"
+ a.aptInstall("wget git gnupg")
+ a.createDir(gopassRootDir, "~/")
+ a.cmd("git init", "~/$gopassRootDir")
+ val fpr = a.gpgFingerprint(publicGPGSnakeoilKey())
+ println("+++++++++++++++++++++++++++++++++++++ $fpr +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++")
+ a.createFile("~/" + gopassRootDir + "/.gpg-id", fpr)
+
+ a.createDir("exampleStoreFolder", "~/")
+ a.createFile("~/exampleStoreFolder/.gpg-id", fpr)
+
+ a.configureGpgKeys(KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())), true)
+
+ // when
+ val res = a.installGopass()
+ val res2 = a.configureGopass(a.userHome() + gopassRootDir)
+ val res3 = a.gopassMountStore("exampleStore", "~/exampleStoreFolder")
+
+ // then
+ a.fileContent("~/.config/gopass/config.yml") // displays the content in the logs
+ assertTrue(res.success)
+ assertTrue(res2.success)
+ assertTrue(res3.success)
+ assertTrue(a.fileContainsText("~/.config/gopass/config.yml", "/home/testuser/.password-store"))
+ assertTrue(a.fileContainsText("~/.config/gopass/config.yml", "exampleStore"))
+ }
+
+ @Test
+ @Disabled // Integrationtest; change user, host and keys, then remove this line to run this test
+ fun test_install_and_configure_Gopass_and_GopassBridgeJsonApi() {
+ // settings to change
+ val host = "192.168.56.135"
+ val user = "xxx"
+ val pubKey = GopassSecretSource("path-to/pub.key").secret()
+ val privateKey = GopassSecretSource("path-to/priv.key").secret()
+
+ // given
+ val a = remote(host, user)
+
+ // when
+ val res = a.def {
+ configureGpgKeys(
+ KeyPair(
+ pubKey,
+ privateKey
+ ),
+ trust = true,
+ skipIfExistin = true
+ )
+ installGopass()
+
+ if (!chk("gopass ls")) {
+ // configure (=init) gopass
+ cmd("printf \"\\ntest\\ntest@test.org\\n\" | gopass init " + gpgFingerprint(pubKey.plain())) // gopass init in default location with gpg-key-fingerprint of given key
+ }
+ downloadGopassBridge()
+ installGopassBridgeJsonApi()
+ configureGopassBridgeJsonApi()
+ }
+
+ // then
+ assertTrue(res.success)
+ }
+}
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/VSCodeKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/VSCodeKtTest.kt
new file mode 100644
index 0000000..493cae7
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/extensions/workplace/base/VSCodeKtTest.kt
@@ -0,0 +1,25 @@
+package org.domaindrivenarchitecture.provs.ubuntu.extensions.workplace.base
+
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.Test
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+
+
+internal class VSCodeKtTest {
+
+ @Test
+ @Disabled("Test currently not working, needs fix. VSC is installed by snapd which is not currently supported to run inside docker")
+ fun installVSC() {
+ // given
+ val a = defaultTestContainer()
+ a.aptInstall("xvfb libgbm-dev libasound2")
+
+ // when
+ val res = a.installVSC("python", "clojure")
+
+ // then
+ assertTrue(res.success)
+ }
+}
\ No newline at end of file
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/filesystem/base/FilesystemKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/filesystem/base/FilesystemKtTest.kt
new file mode 100644
index 0000000..df29d52
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/filesystem/base/FilesystemKtTest.kt
@@ -0,0 +1,194 @@
+package org.domaindrivenarchitecture.provs.ubuntu.filesystem.base
+
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Test
+
+
+internal class FilesystemKtTest {
+
+ @Test
+ @ContainerTest
+ fun checkingCreatingDeletingFile() {
+ // given
+ val prov = defaultTestContainer()
+
+ // when
+ val res1 = prov.fileExists("testfile")
+ val res2 = prov.createFile("testfile", "some content")
+ val res3 = prov.fileExists("testfile")
+ val res4a = prov.fileContainsText("testfile", "some content")
+ val res4b = prov.fileContainsText("testfile", "some non-existing content")
+ val res5 = prov.deleteFile("testfile")
+ val res6 = prov.fileExists("testfile")
+
+ // then
+ assertFalse(res1)
+ assertTrue(res2.success)
+ assertTrue(res3)
+ assertTrue(res4a)
+ assertFalse(res4b)
+ assertTrue(res5.success)
+ assertFalse(res6)
+ }
+
+
+ @Test
+ @ContainerTest
+ fun checkingCreatingDeletingFileWithSudo() {
+ // given
+ val prov = defaultTestContainer()
+
+ // when
+ val file = "/testfile"
+ val res1 = prov.fileExists(file)
+ val res2 = prov.createFile(file, "some content", sudo = true)
+ val res3 = prov.fileExists(file)
+ val res4a = prov.fileContainsText(file, "some content")
+ val res4b = prov.fileContainsText(file, "some non-existing content")
+ val res5 = prov.deleteFile(file)
+ val res6 = prov.fileExists(file)
+ val res7 = prov.deleteFile(file, true)
+ val res8 = prov.fileExists(file)
+
+ // then
+ assertFalse(res1)
+ assertTrue(res2.success)
+ assertTrue(res3)
+ assertTrue(res4a)
+ assertFalse(res4b)
+ assertFalse(res5.success)
+ assertTrue(res6)
+ assertTrue(res7.success)
+ assertFalse(res8)
+ }
+
+
+ @Test
+ @ContainerTest
+ fun checkingCreatingDeletingDir() {
+ // given
+ val prov = defaultTestContainer()
+
+ // when
+ val res1 = prov.dirExists("testdir")
+ val res2 = prov.createDir("testdir", "~/")
+ val res3 = prov.dirExists("testdir")
+ val res4 = prov.deleteDir("testdir", "~/")
+ val res5 = prov.dirExists("testdir")
+
+ val res6 = prov.dirExists("testdir", "~/test")
+ val res7 = prov.createDirs("test/testdir")
+ val res8 = prov.dirExists("testdir", "~/test")
+ prov.deleteDir("testdir", "~/test/")
+
+ // then
+ assertFalse(res1)
+ assertTrue(res2.success)
+ assertTrue(res3)
+ assertTrue(res4.success)
+ assertFalse(res5)
+ assertFalse(res6)
+ assertTrue(res7.success)
+ assertTrue(res8)
+ }
+
+
+ @Test
+ @ContainerTest
+ fun checkingCreatingDeletingDirWithSudo() {
+ // given
+ val prov = defaultTestContainer()
+
+ // when
+ val res1 = prov.dirExists("/testdir", sudo = true)
+ val res2 = prov.createDir("testdir", "/", sudo = true)
+ val res3 = prov.dirExists("/testdir", sudo = true)
+ val res4 = prov.deleteDir("testdir", "/", true)
+ val res5 = prov.dirExists("testdir", sudo = true)
+
+ // then
+ assertFalse(res1)
+ assertTrue(res2.success)
+ assertTrue(res3)
+ assertTrue(res4.success)
+ assertFalse(res5)
+ }
+
+
+ @Test
+ fun userHome() {
+ // given
+ val prov = defaultTestContainer()
+
+ // when
+ val res1 = prov.userHome()
+
+ // then
+ assertEquals("/home/testuser/", res1)
+ }
+
+
+ @Test
+ @ContainerTest
+ fun replaceTextInFile() {
+ // given
+ val prov = defaultTestContainer()
+
+ // when
+ val file = "replaceTest"
+ val res1 = prov.createFile(file, "a\nb\nc\nd")
+ val res2 = prov.replaceTextInFile(file,"b", "hi\nho")
+ val res3 = prov.fileContent(file).equals("a\nhi\nho\nc\nd")
+ val res4 = prov.deleteFile(file)
+
+ // then
+ assertTrue(res1.success)
+ assertTrue(res2.success)
+ assertTrue(res3)
+ assertTrue(res4.success)
+ }
+
+
+ @Test
+ @ContainerTest
+ fun replaceTextInFileRegex() {
+ // given
+ val prov = defaultTestContainer()
+
+ // when
+ val file = "replaceTest"
+ val res1 = prov.createFile(file, "a\nbananas\nc\nd")
+ val res2 = prov.replaceTextInFile(file, Regex("b.*n?nas\n"), "hi\nho\n")
+ val res3 = prov.fileContent(file)
+ val res4 = prov.deleteFile(file)
+
+ // then
+ assertTrue(res1.success)
+ assertTrue(res2.success)
+ assertEquals("a\nhi\nho\nc\nd",res3)
+ assertTrue(res4.success)
+ }
+
+
+ @Test
+ @ContainerTest
+ fun insertTextInFile() {
+ // given
+ val prov = defaultTestContainer()
+
+ // when
+ val file = "insertTest"
+ val res1 = prov.createFile(file, "a\nbananas\nc\nd")
+ val res2 = prov.insertTextInFile(file, Regex("b.*n.nas\n"), "hi\n")
+ val res3 = prov.fileContent(file)
+ val res4 = prov.deleteFile(file)
+
+ // then
+ assertTrue(res1.success)
+ assertTrue(res2.success)
+ assertEquals("a\nbananas\nhi\nc\nd", res3)
+ assertTrue(res4.success)
+ }
+}
\ No newline at end of file
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/git/base/GitKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/git/base/GitKtTest.kt
new file mode 100644
index 0000000..feee9e5
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/git/base/GitKtTest.kt
@@ -0,0 +1,45 @@
+package org.domaindrivenarchitecture.provs.ubuntu.git.base
+
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+import org.domaindrivenarchitecture.provs.ubuntu.keys.base.isHostKnown
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+
+
+internal class GitKtTest {
+
+ @Test
+ fun trustGitServers(){
+ // given
+ val a = defaultTestContainer()
+ a.aptInstall("openssh-client")
+
+ // when
+ val res = a.trustGithub()
+ val known = a.isHostKnown("github.com")
+ val res2 = a.trustGitlab()
+ val known2 = a.isHostKnown("gitlab.com")
+
+ // then
+ assertTrue(res.success)
+ assertTrue(known)
+ assertTrue(res2.success)
+ assertTrue(known2)
+ }
+
+ @Test
+ fun gitClone() {
+ // given
+ val prov = defaultTestContainer()
+ prov.aptInstall("openssh-client ssh git")
+
+ // when
+ prov.trustGithub()
+ prov.gitClone("https://github.com/DomainDrivenArchitecture/dda-git-crate.git", "~/")
+ val res = prov.gitClone("https://github.com/DomainDrivenArchitecture/dda-git-crate.git", "~/")
+
+ // then
+ assertTrue(res.success)
+ }
+}
\ No newline at end of file
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/install/base/InstallKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/install/base/InstallKtTest.kt
new file mode 100644
index 0000000..a70853f
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/install/base/InstallKtTest.kt
@@ -0,0 +1,40 @@
+package org.domaindrivenarchitecture.provs.ubuntu.install.base
+
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.Test
+
+
+internal class InstallKtTest {
+
+ @ContainerTest
+ @Test
+ fun aptInstall_installsPackage() {
+ // given
+ val a = defaultTestContainer()
+
+ // when
+ val res = a.aptInstall("rolldice")
+
+ // then
+ assertTrue(res.success)
+ }
+
+ @ContainerTest
+ @Test
+ @Disabled // run manually if needed;
+ // todo: replace zim by a smaller repo
+ fun aptInstallFromPpa_installsPackage() {
+ // given
+ val a = defaultTestContainer()
+ a.aptInstall("software-properties-common") // prereq for adding a repo to apt
+
+ // when
+ val res = a.aptInstallFromPpa("jaap.karssenberg", "zim", "zim")
+
+ // then
+ assertTrue(res.success)
+ }
+}
\ No newline at end of file
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/keys/ProvisionKeysTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/keys/ProvisionKeysTest.kt
new file mode 100644
index 0000000..da62673
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/keys/ProvisionKeysTest.kt
@@ -0,0 +1,27 @@
+package org.domaindrivenarchitecture.provs.ubuntu.keys
+
+import org.domaindrivenarchitecture.provs.core.Secret
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.condition.EnabledOnOs
+import org.junit.jupiter.api.condition.OS
+
+internal class ProvisionKeysTest {
+
+ @Test
+ @EnabledOnOs(OS.LINUX)
+ fun provisionKeysCurrentUser() {
+ // given
+ val a = defaultTestContainer()
+
+ // when
+ val res = a.provisionKeysCurrentUser(
+ KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())),
+ KeyPair(Secret(publicSSHSnakeoilKey()), Secret(privateSSHSnakeoilKey()))
+ )
+
+ // then
+ assert(res.success)
+ }
+}
+
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/keys/TestKeys.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/keys/TestKeys.kt
new file mode 100644
index 0000000..1140588
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/keys/TestKeys.kt
@@ -0,0 +1,166 @@
+package org.domaindrivenarchitecture.provs.ubuntu.keys
+
+
+fun publicGPGSnakeoilKey(): String {
+ return """-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+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
+KpkCQg==
+=cS1b
+-----END PGP PUBLIC KEY BLOCK----- """
+}
+
+fun privateGPGSnakeoilKey(): String {
+ return """-----BEGIN PGP PRIVATE KEY BLOCK-----
+
+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
+i+XV3Dazj3nq/DxUB0neLU/r1afAEiqZAkI=
+=h5SJ
+-----END PGP PRIVATE KEY BLOCK-----""".trimIndent()
+}
+
+fun publicSSHSnakeoilKey(): String {
+ return """ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDOtQOq8a/Z7SdZVPrh+Icaq5rr+Qg1TZP4IPuRoFgfujUztQ2dy5DfTEbabJ0qHyo+PKwBDQorVohrW7CwvCEVQQh2NLuGgnukBN2ut5Lam7a/fZBoMjAyTvD4bXyEsUr/Bl5CLoBDkKM0elUxsc19ndzSofnDWeGyQjJIWlkNkVk/ybErAnIHVE+D+g3UxwA+emd7BF72RPqdVN39Eu4ntnxYzX0eepc8rkpFolVn6+Ai4CYHE4FaJ7bJ9WGPbwLuDl0pw/Cp3ps17cB+JlQfJ2spOq0tTVk+GcdGnt+mq0WaOnvVeQsGJ2O1HpY3VqQd1AsC2UOyHhAQ00pw7Pi9 snake@oil.com"""
+}
+
+fun privateSSHSnakeoilKey(): String {
+ return """
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIEowIBAAKCAQEAzrUDqvGv2e0nWVT64fiHGqua6/kINU2T+CD7kaBYH7o1M7UN
+ ncuQ30xG2mydKh8qPjysAQ0KK1aIa1uwsLwhFUEIdjS7hoJ7pATdrreS2pu2v32Q
+ aDIwMk7w+G18hLFK/wZeQi6AQ5CjNHpVMbHNfZ3c0qH5w1nhskIySFpZDZFZP8mx
+ KwJyB1RPg/oN1McAPnpnewRe9kT6nVTd/RLuJ7Z8WM19HnqXPK5KRaJVZ+vgIuAm
+ BxOBWie2yfVhj28C7g5dKcPwqd6bNe3AfiZUHydrKTqtLU1ZPhnHRp7fpqtFmjp7
+ 1XkLBidjtR6WN1akHdQLAtlDsh4QENNKcOz4vQIDAQABAoIBAGrgAsZ28gJOcSLq
+ IlGF62zpv0800n6k3tXTT98qtYWqBGn4udKVdxFNYfD7aYNm27OUMSbV9CUWN7Cy
+ lre6fax8lIBxoWfZvU2/ylLUzZREIIf/xxNop6zLTiJUkaYV+P3E8CVt35mPhiLT
+ AYuRL/s8DPnHD9lmdqBxQ4hPVm4Bg7JZxbyN8in3PP1UkdWKxg91O1LYewIZHszq
+ y9BdklKyxQ+fcYP5DD9KkULAjdab48GIxQETrZKp7zV0KiGrjF4Axf5y5yT2jmFT
+ nZ1uZrC1MJTMYyKTBR7wsSpVBMSMUsh5XtxdJo4FuP6g9Kn6AkeQ/Y1shcWVfQgw
+ 6009o8ECgYEA8J1PtnVCHxMLiVKZznzvgCe+EV0RkvuB9PGPdfpLfkHa1DKS+FzH
+ 80D+Vqe0rQNLudG5Qj53MPghNirGyrjXwTYFW9xCqq9hrzfxEI4xIYOd4gHoPMMQ
+ pfWZylP9GYQp/uoa+e/fcdXRSv1IDLRwJZ5XpMtWAIfvMOyDhbfjehECgYEA2+yp
+ poey1y6RWuaIQd2a/PKuYk9jvLEETiz6q7t63MFd6e9cUYX02cG/6yzz6piTWUtx
+ pk9e9IjclLUgV/twVz8SUgSw5TcqBrMnuIT4yQ5rQNZqiEvpCfgb5itcW7I3ADGy
+ dsz2kgaAm7QVZlndQKIy7xRYBCnCD3VQ+TiWh+0CgYAT3qnKg3xmXIhDWtLgvmh4
+ yM9lV64v2R0uQRR7xaOeVYngpByG7gKFEATw2wCMmQ0T10HZOpdVL+huNLId443N
+ osxmfZXzym/irFf36gYcomXTWBz5h5JEYjfFAZKRHNzq9CIuKaTmHaYe7zOX+P6Z
+ 3K2YKkJ74L3b6GwkCr96QQKBgQC6n0iTTSGg4h5skaXcpq2HqnP6br4G9/vcTuTk
+ Z/JpdBk6k2i2sULGqlguu/W8BH89Tf0CEOZWAfGUq2Ln5jE9iAMG4H4v9DDQgKTb
+ OtNW4cp3uburLydw0z7xgagdE80CeCmmEGXIIoZuGlHyiZ1r5HfuU0ghOEI6FeaB
+ pdhvPQKBgEpmHV66wqSzzxmYxKjUu8gl9rIniG8SWXHlvcoGVwt1qdOMtNtvwDgB
+ DnbUbANSjzIfFSqVwlx7nXG1e1yN7F1YuyUa3I5QEm4+5URoTSDghk03LTFH+kfM
+ OUxwE8Su4WnoQc7WjkTG0M3FECAu7TEcF9uqdcEsW+4+JMAhE5oo
+ -----END RSA PRIVATE KEY-----
+ """.trimIndent()
+}
\ No newline at end of file
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/keys/base/GpgKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/keys/base/GpgKtTest.kt
new file mode 100644
index 0000000..f76e6e4
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/keys/base/GpgKtTest.kt
@@ -0,0 +1,80 @@
+package org.domaindrivenarchitecture.provs.ubuntu.keys.base
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.Secret
+import org.domaindrivenarchitecture.provs.core.processors.ContainerStartMode
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
+import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
+import org.domaindrivenarchitecture.provs.ubuntu.keys.KeyPair
+import org.domaindrivenarchitecture.provs.ubuntu.keys.privateGPGSnakeoilKey
+import org.domaindrivenarchitecture.provs.ubuntu.keys.publicGPGSnakeoilKey
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+
+internal class GpgKtTest {
+
+ @Test
+ @ContainerTest
+ fun gpgFingerprint_returnsCorrectFingerprint() {
+ // given
+ val a = defaultTestContainer()
+ a.aptInstall("gpg")
+ a.cmd("gpg --version") // just for info reasons
+
+ // when
+ val fingerprint = a.gpgFingerprint(publicGPGSnakeoilKey())
+
+ // then
+ assertEquals("85052C6954262D61D4E9977E0674104CA81A4905", fingerprint)
+ }
+
+
+ @Test
+ @ContainerTest
+ fun configureGpgKeys() {
+ // given
+ val a = defaultTestContainer()
+
+ // when
+ val res = a.configureGpgKeys(KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())))
+
+ // then
+ assertTrue(res.success)
+ }
+
+
+ @Test
+ @ContainerTest
+ fun configureGpgKeysTrusted() {
+ // given
+ val a = defaultTestContainer()
+
+ // when
+ val res = a.configureGpgKeys(KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())), true)
+
+ // then
+ assertTrue(res.success)
+ val trustedKey = a.cmd("gpg -K | grep ultimate").out
+ assertEquals("uid [ultimate] snakeoil ", trustedKey?.trim())
+ }
+
+
+ @Test
+ @ContainerTest
+ fun configureGpgKeysIsIdempotent() {
+ // given
+ val a = defaultTestContainer()
+
+ // when
+ val res = a.configureGpgKeys(KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())))
+ val res2 = a.configureGpgKeys(KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())))
+
+ // then
+ assertTrue(res.success)
+ assertTrue(res2.success)
+ }
+}
+
+
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/keys/base/SshKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/keys/base/SshKtTest.kt
new file mode 100644
index 0000000..88b542c
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/keys/base/SshKtTest.kt
@@ -0,0 +1,24 @@
+package org.domaindrivenarchitecture.provs.ubuntu.keys.base
+
+import org.domaindrivenarchitecture.provs.core.Secret
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+import org.domaindrivenarchitecture.provs.ubuntu.keys.*
+import org.junit.jupiter.api.Test
+
+import org.junit.jupiter.api.Assertions.*
+
+internal class SshKtTest {
+
+ @Test
+ fun configureSshKeys() {
+ // given
+ val a = defaultTestContainer()
+
+ // when
+ val res = a.configureSshKeys(KeyPair(Secret(publicSSHSnakeoilKey()), Secret(privateSSHSnakeoilKey())))
+
+ // then
+ assertTrue(res.success)
+
+ }
+}
\ No newline at end of file
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/secret/secretSources/PromptSecretSourceTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/secret/secretSources/PromptSecretSourceTest.kt
new file mode 100644
index 0000000..3374435
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/secret/secretSources/PromptSecretSourceTest.kt
@@ -0,0 +1,13 @@
+package org.domaindrivenarchitecture.provs.ubuntu.secret.secretSources
+
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.Test
+
+internal class PromptSecretSourceTest {
+
+ @Test
+ @Disabled // run manually
+ fun secret() {
+ println("Secret: " + PromptSecretSource().secret().plain())
+ }
+}
\ No newline at end of file
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/user/ProvisionUserKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/user/ProvisionUserKtTest.kt
new file mode 100644
index 0000000..be3f1db
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/user/ProvisionUserKtTest.kt
@@ -0,0 +1,33 @@
+package org.domaindrivenarchitecture.provs.ubuntu.user
+
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+import org.domaindrivenarchitecture.provs.ubuntu.keys.*
+import org.domaindrivenarchitecture.provs.ubuntu.secret.SecretSourceType
+import org.domaindrivenarchitecture.provs.ubuntu.user.base.configureUser
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.condition.EnabledOnOs
+import org.junit.jupiter.api.condition.OS
+
+
+internal class ProvisionUserKtTest {
+
+ @Test
+ @EnabledOnOs(OS.LINUX)
+ fun configureUser() {
+ // given
+ val a = defaultTestContainer()
+
+ // when
+ val res = a.configureUser(
+ UserConfig(
+ "testuser",
+ "test@mail.com",
+ KeyPairSource(SecretSourceType.PLAIN, publicGPGSnakeoilKey(), privateGPGSnakeoilKey()),
+ KeyPairSource(SecretSourceType.PLAIN, publicSSHSnakeoilKey(), privateSSHSnakeoilKey())
+ )
+ )
+
+ // then
+ assert(res.success)
+ }
+}
\ No newline at end of file
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/utils/UtilsKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/utils/UtilsKtTest.kt
new file mode 100644
index 0000000..8c39d98
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/utils/UtilsKtTest.kt
@@ -0,0 +1,23 @@
+package org.domaindrivenarchitecture.provs.ubuntu.utils
+
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+
+internal class UtilsKtTest {
+
+ @ContainerTest
+ @Test
+ fun printToShell_escapes_successfully() {
+ // given
+ val a = Prov.defaultInstance()
+
+ // when
+ val testString = "test if newline \n and apostrophe's ' \" and special chars !§$%[]\\ äöüß are handled correctly"
+ val res = a.cmd(printToShell(testString)).out
+
+ // then
+ assertEquals(testString, res)
+ }
+}
\ No newline at end of file
diff --git a/bin/test/org/domaindrivenarchitecture/provs/ubuntu/web/base/WebKtTest.kt b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/web/base/WebKtTest.kt
new file mode 100644
index 0000000..07f5879
--- /dev/null
+++ b/bin/test/org/domaindrivenarchitecture/provs/ubuntu/web/base/WebKtTest.kt
@@ -0,0 +1,30 @@
+package org.domaindrivenarchitecture.provs.ubuntu.web.base
+
+import org.domaindrivenarchitecture.provs.test.defaultTestContainer
+import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createFile
+import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.fileContent
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+
+internal class WebKtTest {
+
+ @ContainerTest
+ @Test
+ fun downloadFromURL_downloadsFile() {
+ // given
+ val a = defaultTestContainer()
+ val file = "file1"
+ a.createFile("/tmp/" + file, "hello")
+
+ // when
+ val res = a.downloadFromURL("file:///tmp/" + file, "file2", "/tmp")
+
+ // then
+ val res2 = a.fileContent("/tmp/file2")
+
+ assertTrue(res.success)
+ assertEquals("hello", res2)
+ }
+}
\ No newline at end of file
diff --git a/bin/testFixtures/logback-test.xml b/bin/testFixtures/logback-test.xml
new file mode 100644
index 0000000..a43a2a2
--- /dev/null
+++ b/bin/testFixtures/logback-test.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ System.err
+
+
+ %d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{35}) - %msg %n
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bin/testFixtures/org/domaindrivenarchitecture/provs/test/TestSetup.kt b/bin/testFixtures/org/domaindrivenarchitecture/provs/test/TestSetup.kt
new file mode 100644
index 0000000..5cf6261
--- /dev/null
+++ b/bin/testFixtures/org/domaindrivenarchitecture/provs/test/TestSetup.kt
@@ -0,0 +1,35 @@
+package org.domaindrivenarchitecture.provs.test
+
+import org.domaindrivenarchitecture.provs.core.ProgressType
+import org.domaindrivenarchitecture.provs.core.Prov
+import org.domaindrivenarchitecture.provs.core.docker.dockerImageExists
+import org.domaindrivenarchitecture.provs.core.docker.dockerProvideImage
+import org.domaindrivenarchitecture.provs.core.docker.dockerimages.UbuntuPlusUser
+import org.domaindrivenarchitecture.provs.core.processors.ContainerStartMode
+import org.domaindrivenarchitecture.provs.core.processors.ContainerUbuntuHostProcessor
+
+val testDockerWithSudo = !"true".equals(System.getProperty("testdockerwithoutsudo")?.toLowerCase())
+
+const val defaultTestContainerName = "provs_test"
+
+fun defaultTestContainer(startMode: ContainerStartMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE): Prov {
+ val image = UbuntuPlusUser()
+ val prov = testLocal()
+ if (!prov.dockerImageExists(image.imageName(), testDockerWithSudo)) {
+ prov.dockerProvideImage(image, sudo = testDockerWithSudo)
+ }
+
+ return Prov.newInstance(
+ ContainerUbuntuHostProcessor(
+ defaultTestContainerName,
+ startMode = startMode,
+ sudo = testDockerWithSudo,
+ dockerImage = image.imageName()
+ ),
+ progressType = ProgressType.NONE
+ )
+}
+
+fun testLocal(): Prov {
+ return Prov.newInstance(name = "testing", progressType = ProgressType.NONE)
+}
\ No newline at end of file
diff --git a/bin/testFixtures/org/domaindrivenarchitecture/provs/test/tags/Tags.kt b/bin/testFixtures/org/domaindrivenarchitecture/provs/test/tags/Tags.kt
new file mode 100644
index 0000000..bc24bc5
--- /dev/null
+++ b/bin/testFixtures/org/domaindrivenarchitecture/provs/test/tags/Tags.kt
@@ -0,0 +1,21 @@
+package org.domaindrivenarchitecture.provs.test.tags
+
+import org.junit.jupiter.api.Tag
+import org.junit.jupiter.api.Test
+
+private const val CONTAINER_TEST = "containertest"
+private const val CONTAINER_TEST_NON_CI = "containernonci"
+
+
+@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
+@kotlin.annotation.Retention
+@Tag(CONTAINER_TEST)
+@Test
+annotation class ContainerTest
+
+
+@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
+@kotlin.annotation.Retention
+@Tag(CONTAINER_TEST_NON_CI)
+@Test
+annotation class NonCi
\ No newline at end of file