You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
provs/src/main/kotlin/io/provs/Prov.kt

362 lines
12 KiB
Kotlin

package io.provs
import io.provs.platforms.SHELL
import io.provs.platforms.UbuntuProv
import io.provs.platforms.WinProv
import io.provs.processors.LocalProcessor
import io.provs.processors.Processor
enum class ResultMode { NONE, LAST, ALL, FAILEXIT }
enum class OS { WINDOWS, LINUX }
/**
* This main class offers methods to execute shell commands either locally or remotely (via ssh) or in a docker
* depending on the processor which is passed to the constructor.
*/
open class Prov protected constructor(private val processor: Processor, val name: String? = null) {
companion object Factory {
lateinit var prov: Prov
fun defaultInstance(platform: String? = null): Prov {
return if (::prov.isInitialized) {
prov
} else {
prov = newInstance(platform = platform, name = "default instance")
prov
}
}
fun newInstance(processor: Processor = LocalProcessor(), platform: String? = null, name: String? = null): Prov {
val os = platform ?: System.getProperty("os.name")
return when {
os.toUpperCase().contains(OS.LINUX.name) -> UbuntuProv(processor, name)
os.toUpperCase().contains(OS.WINDOWS.name) -> WinProv(processor, name)
else -> throw Exception("OS not supported")
}
}
}
private val internalResults = arrayListOf<InternalResult>()
private var level = 0
private var previousLevel = 0
private var exit = false
private var runInContainerWithName: String? = null
// task defining functions
fun def(a: Prov.() -> ProvResult): ProvResult {
return handle(ResultMode.ALL) { a() }
}
fun requireLast(a: Prov.() -> ProvResult): ProvResult {
return handle(ResultMode.LAST) { a() }
}
fun optional(a: Prov.() -> ProvResult): ProvResult {
return handle(ResultMode.NONE) { a() }
}
fun requireAll(a: Prov.() -> ProvResult): ProvResult {
return handle(ResultMode.ALL) { a() }
}
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 programs
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
)
}
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 = result.argsToString(),
out = result.out,
err = result.err
)
}
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 = " ")
}
/**
* Execute commands 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): 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): 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): ProvResult {
throw Exception("Not implemented")
}
/**
* Executes command cmd and returns true in case of success else false.
* The success resp. failure does not count into the overall success.
*/
fun check(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)
addResultToEval(ProvResult(result.success, err = result.err, exception = result.exception, exit = result.exit))
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 an 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) = 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).success
}
}
ProvResult(success)
}
/**
* 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
ProgressBar.init()
}
// pre-handling
val resultIndex = internalResults.size
val method = getCallingMethodName()
internalResults.add(InternalResult(level, method, null))
previousLevel = level
level++
// call the actual function
val res = if (!exit) {
ProgressBar.progress()
@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) {
if (res.success) {
return ProvResult(true)
} else {
exit = true
return ProvResult(false)
}
} else {
ProvResult(false, err = "mode unknown")
}
previousLevel = level
internalResults[resultIndex].provResult = returnValue
if (level == 0) {
ProgressBar.end()
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 data class InternalResult(val level: Int, val method: String?, var provResult: ProvResult?) {
override fun toString() : String {
val provResult = provResult
if (provResult != null) {
return prefix(level) + (if (provResult.success) "Success -- " else "FAILED -- ") +
method + " " + (provResult.cmd ?: "") +
(if (!provResult.success && provResult.err != null) " -- Error: " + provResult.err.escapeNewline() else "") }
else
return prefix(level) + " " + method + " " + "... in progress ... "
}
private fun prefix(level: Int): String {
return "---".repeat(level) + "> "
}
}
val ANSI_RESET = "\u001B[0m"
val ANSI_BRIGHT_RED = "\u001B[91m"
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.get(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 object ProgressBar {
fun init() {
print("Processing started ...\n")
}
fun progress() {
print(".")
System.out.flush()
}
fun end() {
println("processing completed.")
}
}