refactor imports to org.domaindrivenarchitecture

This commit is contained in:
leo 2021-08-07 19:59:12 +02:00
parent 3810fe6723
commit 728e2a8d8f
101 changed files with 6288 additions and 0 deletions

46
.classpath Normal file
View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="bin/main" path="src/main/kotlin">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/main" path="src/main/resources">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/kotlin">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/resources">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/testFixtures" path="src/testFixtures/kotlin">
<attributes>
<attribute name="gradle_scope" value="testFixtures"/>
<attribute name="gradle_used_by_scope" value="testFixtures"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/testFixtures" path="src/testFixtures/resources">
<attributes>
<attribute name="gradle_scope" value="testFixtures"/>
<attribute name="gradle_used_by_scope" value="testFixtures"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

34
.project Normal file
View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>provs-core</name>
<comment>Project provs-core created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
<filteredResources>
<filter>
<id>1628357951205</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

View file

@ -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

53
bin/main/logback.xml Normal file
View file

@ -0,0 +1,53 @@
<configuration>
<timestamp key="byTime" datePattern="yyyyMMdd'T'HHmmss"/>
<appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{35}) - %msg %n</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<target>System.out</target>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{35}) - %msg %n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>./logs/provs-${byTime}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>./logs/provs-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>3</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<append>true</append>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="net.schmizz" level="ERROR">
<appender-ref ref="STDERR" />
<appender-ref ref="FILE" />
</logger>
<root level="INFO">
<appender-ref ref="STDERR" />
<appender-ref ref="FILE" />
</root>
<root level="WARN">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View file

@ -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<ResultLine>()
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<String> {
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()
}
}

View file

@ -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 ""
}
}

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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
"""
}
}

View file

@ -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")
}

View file

@ -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<Class<*>>(
Array<String>::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<String>())
} else {
method.invoke(null, args.drop(2).toTypedArray())
}
} else {
println("Usage: <packageName.className> <optional static methodName> <optional parameters ... >")
}
}

View file

@ -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()
}

View file

@ -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")
}
}

View file

@ -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()
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -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<out String> = 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
}
}

View file

@ -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()
}
}

View file

@ -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

View file

@ -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()
}

View file

@ -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
}

View file

@ -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())
}

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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")
}

View file

@ -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.")
}

View file

@ -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)
}
}
}

View file

@ -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";
}
}
}
"""
}

View file

@ -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()
}

View file

@ -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()}")
}

View file

@ -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";
}
}
}
"""
)
}

View file

@ -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)
}
}

View file

@ -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"))
}

View file

@ -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)
}

View file

@ -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
"""
}

View file

@ -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=<job_name>` 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']
"""

View file

@ -0,0 +1,5 @@
package org.domaindrivenarchitecture.provs.ubuntu.extensions.server_software.prometheus.base
val prometheusNginxConfig = """
proxy_pass http://localhost:9090/prometheus;
"""

View file

@ -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 <ip> <user>
* 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<String>) {
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<String>)
*
* 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<String>) {
provisionRemote(args = args)
}

View file

@ -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)) }
}

View file

@ -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")
}

View file

@ -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")
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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"

View file

@ -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()

View file

@ -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<String>) = 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/")
}

View file

@ -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)
}

View file

@ -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")
}
}

View file

@ -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()
}

View file

@ -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)
}

View file

@ -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<String>) = 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"))
}

View file

@ -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")
}

View file

@ -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
}

View file

@ -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) }
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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)) }
}

View file

@ -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)
}

View file

@ -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()}\""
}

View file

@ -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)
}
}

View file

@ -0,0 +1,7 @@
{
"type": "MINIMAL",
"ssh": null,
"gpg": null,
"gitUserName": "mygitusername",
"gitEmail": "my@git.email"
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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<String>) {
println("test is fun " + args.joinToString(" "))
}
@Suppress("unused")
fun main(args: Array<String>) {
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())
}
}

View file

@ -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")
}
}

View file

@ -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())
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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())
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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())
}
}

View file

@ -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()
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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")
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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 <snake@oil.com>", 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)
}
}

View file

@ -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)
}
}

View file

@ -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())
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -0,0 +1,19 @@
<configuration>
<timestamp key="byTime" datePattern="yyyyMMdd'T'HHmmss"/>
<appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{35}) - %msg %n</pattern>
</encoder>
</appender>
<logger name="net.schmizz" level="ERROR"/>
<root level="ALL">
<appender-ref ref="STDERR" />
</root>
</configuration>

View file

@ -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)
}

Some files were not shown because too many files have changed in this diff Show more