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