initial commit

merge-requests/1/merge
az 3 years ago
parent d1af64aa8f
commit 4766ffe18e

5
.gitignore vendored

@ -0,0 +1,5 @@
/.gradle/*
/build/*
/out/*
/.idea/*
*.log

@ -0,0 +1,52 @@
image: openjdk:15-jdk-slim
stages:
- build
- test
- jar
- publish
before_script:
- echo "---------- Start CI ----------"
- export GRADLE_USER_HOME=`pwd`/.gradle
- export repoUser="$repoUser"
- export repoPassword="$repoPassword"
- chmod +x gradlew
build:
stage: build
script:
- echo "---------- build stage ----------"
- ./gradlew --build-cache assemble
artifacts:
paths:
- build/libs/*.jar
expire_in: 1 week
test:
stage: test
services:
- docker:dind
dependencies:
- build
script:
- ./gradlew test
jar:
stage: jar
script:
- echo "---------- jar ----------"
- ./gradlew jar
artifacts:
paths:
- build/libs/*.jar
expire_in: 2 months
publish:
stage: publish
script:
- echo "---------- publish ----------"
- ./gradlew -PrepoUser="$repoUser" -PrepoPassword="$repoPassword" publish
after_script:
- echo "---------- End CI ----------"

@ -0,0 +1,52 @@
# Provs-core
the core engine of the provs framework.
## Provs framework
Framework for automating shell- and other system-tasks for provisioning reasons or other purposes.
Can easily be run
* locally
* remotely
* in a docker container
* or with any self-defined custom processor
Combines the
* convenience and robustness of a modern programming language (Kotlin) with
* power of being able to use shell commands and with
* clear and detailed result summary of the built-in failure handling.
## Provs-core
Provs-core provides the core component with
* execution engine
* failure handling
* multiple execution processors
* execution summary and logging
* support for secrets
## Usage
### Run hello world
Locally:
`kotlinc -cp build/libs/provs-latest.jar -script scripts/HelloWorldLocal.kts`
Remotely:
`kotlinc -cp build/libs/provs-latest.jar -script scripts/HelloWorldRemote.kts`
### Other examples
For a bunch of usage examples please have a look at provs-ubuntu-extensions.
## License
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
[Apache license 2.0](http://www.apache.org/licenses/LICENSE-2.0)
Unless required by applicable law or agreed to in writing, software distributed under the License
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.

@ -0,0 +1,154 @@
buildscript {
ext.kotlin_version = '1.4.21'
repositories { jcenter() }
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
}
}
apply plugin: 'idea'
apply plugin: 'org.jetbrains.kotlin.jvm'
apply plugin: 'org.jetbrains.kotlin.plugin.serialization'
apply plugin: 'maven-publish'
group = 'io.provs'
version = '0.8.5-SNAPSHOT'
repositories {
mavenCentral()
jcenter()
}
test {
useJUnitPlatform {
excludeTags('containertest')
}
}
compileJava.options.debugOptions.debugLevel = "source,lines,vars"
compileTestJava.options.debugOptions.debugLevel = "source,lines,vars"
// https://stackoverflow.com/questions/21904269/configure-gradle-to-publish-sources-and-javadoc
java {
withSourcesJar()
withJavadocJar()
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0" // JVM dependency
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation group: 'com.hierynomus', name: 'sshj', version: '0.30.0'
api "org.slf4j:slf4j-api:1.7.30"
api "ch.qos.logback:logback-classic:1.2.3"
api "ch.qos.logback:logback-core:1.2.3"
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.5.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2'
testImplementation "io.mockk:mockk:1.9.3"
}
//create a single Jar with all dependencies excl. Kotlin libs
task fatJar(type: Jar) {
manifest {
attributes 'Implementation-Title': 'Gradle Jar File Example',
'Implementation-Version': project.version,
'Main-Class': 'io.provs.entry.EntryKt'
}
archivesBaseName = project.name + '-all'
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
task fatJarLatest(type: Jar) {
doFirst {
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
}
exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA'
manifest {
attributes 'Implementation-Title': 'Gradle Jar File Example',
'Implementation-Version': project.version,
'Main-Class': 'io.provs.entry.EntryKt'
}
with jar
archiveFileName = 'provs-fat-latest.jar'
}
//create a single Jar with all dependencies incl. Kotlin libs
task uberJar(type: Jar) {
from sourceSets.main.output
dependsOn configurations.runtimeClasspath
from {
configurations.runtimeClasspath.findAll { it.name.endsWith('jar') }.collect { zipTree(it) }
} {
exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA'
}
manifest {
attributes 'Implementation-Title': 'Gradle Jar File Example',
'Implementation-Version': project.version,
'Main-Class': 'io.provs.entry.EntryKt'
}
archiveClassifier = 'uber'
}
//create a single Jar with all dependencies incl. Kotlin libs - as ...-latest
task uberJarLatest(type: Jar) {
from sourceSets.main.output
dependsOn configurations.runtimeClasspath
from {
configurations.runtimeClasspath.findAll { it.name.endsWith('jar') }.collect { zipTree(it) }
} {
exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA'
}
manifest {
attributes 'Implementation-Title': 'Gradle Jar File Example',
'Implementation-Version': project.version,
'Main-Class': 'io.provs.entry.EntryKt'
}
archiveFileName = 'provs-latest.jar'
}
task sourceJar(type: Jar, dependsOn: classes) {
from sourceSets.main.allSource
archiveClassifier.set("sources")
}
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
}
}
repositories {
if (System.getenv("CI_JOB_TOKEN") != null) {
// see https://docs.gitlab.com/ee/user/packages/maven_repository/index.html
maven {
url "https://gitlab.com/api/v4/projects/23127098/packages/maven"
name "GitLab"
credentials(HttpHeaderCredentials) {
name = 'Job-Token'
value = System.getenv("CI_JOB_TOKEN")
}
// project id: 23966425
authentication {
header(HttpHeaderAuthentication)
}
}
}
}
}

@ -0,0 +1,4 @@
kotlin.code.style=official
#avoid nexus error "Cannot upload checksum for snapshot-maven-metadata.xml. Remote repository doesn't support sha-512. Error: Could not PUT ..."
systemProp.org.gradle.internal.publish.checksums.insecure=true

Binary file not shown.

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

188
gradlew vendored

@ -0,0 +1,188 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

100
gradlew.bat vendored

@ -0,0 +1,100 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem http://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

@ -0,0 +1 @@
rootProject.name = 'provs-core'

@ -0,0 +1,361 @@
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() }
}
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.")
}
}

@ -0,0 +1,30 @@
package io.provs
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() } ?: "")
}
fun toShortString() : String {
return "ProvResult:: ${if (success) "Succeeded" else "FAILED"} -- ${if (!success && (out != null)) "Details: $out" else ""}"
}
}
@Suppress("unused") // might be used by custom methods
data class TypedResult<T>(val success: Boolean, val resultObject: T? = null) {
override fun toString(): String {
return "TypedResult:: ${if (success) "Succeeded" else "FAILED"} -- Result object: " + resultObject?.run { toString().escapeNewline() }
}
}

@ -0,0 +1,14 @@
package io.provs
open class Secret(private val value: String) {
override fun toString(): String {
return "********"
}
fun plain() : String {
return value
}
}
class Password(plainPassword: String) : Secret(plainPassword)

@ -0,0 +1,107 @@
package io.provs
import io.provs.docker.provideContainer
import io.provs.processors.ContainerStartMode
import io.provs.processors.ContainerUbuntuHostProcessor
import io.provs.processors.RemoteProcessor
import java.io.File
/**
* 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.
*/
@Suppress("unused") // used by other libraries resp. KotlinScript
fun remote(host: String, remoteUser: String, password: Secret? = null, platform: String? = null): Prov {
return Prov.newInstance(RemoteProcessor(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.
*/
@Suppress("unused") // 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)
}
}

@ -0,0 +1,87 @@
package io.provs.docker
import io.provs.Prov
import io.provs.ProvResult
import io.provs.docker.images.DockerImage
import io.provs.docker.platforms.*
import io.provs.platforms.UbuntuProv
import io.provs.processors.ContainerStartMode
/**
* 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
) : ProvResult {
if (this is UbuntuProv) {
return this.provideContainerPlatform(containerName, imageName, startMode)
} else {
throw RuntimeException("docker not yet supported for " + (this as UbuntuProv).javaClass)
}
}
fun Prov.containerRuns(containerName: String) : Boolean {
if (this is UbuntuProv) {
return this.containerRunsPlatform(containerName)
} else {
throw RuntimeException("docker not yet supported for " + (this as UbuntuProv).javaClass)
}
}
fun Prov.runContainer(
containerName: String = "defaultProvContainer",
imageName: String = "ubuntu"
) : ProvResult {
if (this is UbuntuProv) {
return this.runContainerPlatform(containerName, imageName)
} else {
throw RuntimeException("docker not yet supported for " + (this as UbuntuProv).javaClass)
}
}
fun Prov.containerSh(containerName: String, cmd: String) : ProvResult {
if (this is UbuntuProv) {
return this.containerShPlatform(containerName, cmd)
} else {
throw RuntimeException("docker not yet supported for " + (this as UbuntuProv).javaClass)
}
}
fun Prov.dockerBuildImage(image: DockerImage, skipIfExisting: Boolean = true) : ProvResult {
if (this is UbuntuProv) {
return this.dockerBuildImagePlatform(image, skipIfExisting)
} else {
throw RuntimeException("docker not yet supported for " + (this as UbuntuProv).javaClass)
}
}
fun Prov.dockerImageExists(imageName: String) : Boolean {
if (this is UbuntuProv) {
return this.dockerImageExistsPlatform(imageName)
} else {
throw RuntimeException("docker not yet supported for " + (this as UbuntuProv).javaClass)
}
}
fun Prov.exitAndRmContainer(
containerName: String
) : ProvResult {
if (this is UbuntuProv) {
return this.exitAndRmContainerPlatform(containerName)
} else {
throw RuntimeException("docker not yet supported for " + (this as UbuntuProv).javaClass)
}
}

@ -0,0 +1,34 @@
package io.provs.docker.images
interface DockerImage {
fun imageName() : String
fun imageText() : String
}
/**
* Dockerfile based ubuntu image added by a non-root default user.
*/
@Suppress("unused") // used by other libraries
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
"""
}
}

@ -0,0 +1,92 @@
package io.provs.docker.platforms
import io.provs.*
import io.provs.docker.containerRuns
import io.provs.docker.dockerImageExists
import io.provs.docker.exitAndRmContainer
import io.provs.docker.images.DockerImage
import io.provs.platforms.UbuntuProv
import io.provs.processors.ContainerStartMode
fun UbuntuProv.provideContainerPlatform(
containerName: String,
imageName: String = "ubuntu",
startMode: ContainerStartMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE
): ProvResult = requireLast {
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(
"sudo docker run -dit --name=$containerName $imageName"
).success
) {
throw RuntimeException("could not start docker")
}
} else if (startMode == ContainerStartMode.USE_RUNNING_ELSE_CREATE) {
val r =
cmd("sudo docker inspect -f '{{.State.Running}}' $containerName")
if (!r.success || "false\n" == r.out) {
cmd("sudo docker rm -f $containerName")
cmd("sudo docker run -dit --name=$containerName $imageName")
}
}
ProvResult(containerRuns(containerName))
}
fun UbuntuProv.containerRunsPlatform(containerName: String): Boolean {
return cmdNoEval("sudo docker inspect -f '{{.State.Running}}' $containerName").out?.equals("true\n") ?: false
}
fun UbuntuProv.runContainerPlatform(
containerName: String = "defaultProvContainer",
imageName: String = "ubuntu"
) = def {
cmd("sudo docker run -dit --name=$containerName $imageName")
}
fun UbuntuProv.containerExecPlatform(containerName: String, cmd: String) = def {
cmd("sudo docker exec $containerName $cmd")
}
fun UbuntuProv.containerShPlatform(containerName: String, cmd: String) = def {
containerExecPlatform(containerName, "sh -c \"${cmd.escapeDoubleQuote()}\"")
}
fun UbuntuProv.dockerBuildImagePlatform(image: DockerImage, skipIfExisting: Boolean): ProvResult {
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 && sudo docker build --tag ${image.imageName()} .")
}
fun UbuntuProv.dockerImageExistsPlatform(imageName: String): Boolean {
return (cmd("sudo docker images $imageName -q").out != "")
}
fun UbuntuProv.exitAndRmContainerPlatform(
containerName: String
) = requireAll {
if (containerRuns(containerName)) {
cmd("sudo docker stop $containerName")
}
cmd("sudo docker rm $containerName")
}

@ -0,0 +1,37 @@
package io.provs.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] class to be called
* @param args[1] (optional) static method of the class; if not specified, the "main" method is used
* @param args[2...] String parameters that are passed to the method, only applicable if method has vararg parameter
*/
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
(if (args.size == 2 && args[1] != "main") jClass.getMethod(args[1])
else jClass.getMethod(args[1], *parameterTypeStringArray))
if ((args.size == 1) || (args.size == 2 && args[1] == "main")) {
method.invoke(null, emptyArray<String>())
} else if (args.size == 2) {
method.invoke(null)
} else {
method.invoke(null, args.drop(2).toTypedArray())
}
} else {
println("Usage: <packageName.className> <static methodName>")
}
}

@ -0,0 +1,24 @@
package io.provs.platforms
import io.provs.Prov
import io.provs.ProvResult
import io.provs.processors.LocalProcessor
import io.provs.processors.Processor
const val SHELL = "/bin/bash" // could be changed to another shell like "sh", "/bin/csh" if required
class UbuntuProv internal constructor(processor : Processor = LocalProcessor(), name: String? = null) : Prov (processor, name) {
override fun cmd(cmd: String, dir: String?) : ProvResult = def {
xec(SHELL, "-c", if (dir == null) cmd else "cd $dir && $cmd")
}
override fun cmdNoLog(cmd: String, dir: String?) : ProvResult {
return xecNoLog(SHELL, "-c", if (dir == null) cmd else "cd $dir && $cmd")
}
override fun cmdNoEval(cmd: String, dir: String?) : ProvResult {
return xec(SHELL, "-c", if (dir == null) cmd else "cd $dir && $cmd")
}
}

@ -0,0 +1,25 @@
package io.provs.platforms
import io.provs.Prov
import io.provs.ProvResult
import io.provs.processors.LocalProcessor
import io.provs.processors.Processor
class WinProv internal constructor(processor : Processor = LocalProcessor(), name: String? = null) : Prov (processor, name) {
// todo put cmd.exe in variable SHELL
override fun cmd(cmd: String, dir: String?) : ProvResult = def {
xec("cmd.exe", "/c", if (dir == null) cmd else "cd $dir && $cmd")
}
override fun cmdNoLog(cmd: String, dir: String?) : ProvResult = def {
xecNoLog("cmd.exe", "/c", if (dir == null) cmd else "cd $dir && $cmd")
}
override fun cmdNoEval(cmd: String, dir: String?) : ProvResult {
return xec("cmd.exe", "/c", if (dir == null) cmd else "cd $dir && $cmd")
}
}

@ -0,0 +1,81 @@
package io.provs.processors
import io.provs.Prov
import io.provs.docker.containerSh
import io.provs.docker.provideContainer
import io.provs.escapeAndEncloseByDoubleQuoteForShell
import io.provs.platforms.SHELL
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",
@Suppress("unused") // suppress false positive warning
private val dockerImage: String = "ubuntu",
@Suppress("unused") // suppress false positive warning
private val startMode: ContainerStartMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE,
private val endMode: ContainerEndMode = ContainerEndMode.KEEP_RUNNING
) : Processor {
private var localExecution = LocalProcessor()
private var a = Prov.newInstance(name = "ContainerUbuntuHostProcessor")
init {
val r = a.provideContainer(containerName, dockerImage, startMode)
if (!r.success)
throw RuntimeException("Could not start docker image: " + r.toShortString(), r.exception)
}
override fun x(vararg args: String): ProcessResult {
return localExecution.x("sh", "-c", "sudo docker exec $containerName " + buildCommand(*args))
}
override fun xNoLog(vararg args: String): ProcessResult {
return localExecution.xNoLog("sh", "-c", "sudo docker exec $containerName " + buildCommand(*args))
}
fun installSudo(): ContainerUbuntuHostProcessor {
a.containerSh(containerName, "apt-get update")
a.containerSh(containerName, "apt-get -y install sudo")
return this
}
fun addAndSwitchToUser(user: String = "testuser"): ContainerUbuntuHostProcessor {
a.containerSh(containerName,"sudo useradd -m $user && echo '$user:$user' | chpasswd && adduser $user sudo")
a.containerSh(containerName,"echo '$user ALL=(ALL:ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/$user")
a.containerSh(containerName,"sudo su $user")
a.containerSh(containerName,"cd /home/$user")
a.containerSh(containerName,"mkdir $user && cd $user")
return this
}
fun exitAndRm() {
localExecution.x(SHELL, "-c", "sudo docker stop $containerName")
localExecution.x(SHELL, "-c", "sudo docker 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()
}
}
}

@ -0,0 +1,77 @@
package io.provs.processors
import io.provs.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 = 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)
}
}
}

@ -0,0 +1,19 @@
package io.provs.processors
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)
}
}

@ -0,0 +1,54 @@
package io.provs.processors
interface Processor {
fun x(vararg args: String): ProcessResult
fun xNoLog(vararg args: String): ProcessResult
fun close() {}
}
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
}
}

@ -0,0 +1,127 @@
package io.provs.processors
import io.provs.Secret
import io.provs.escapeAndEncloseByDoubleQuoteForShell
import io.provs.escapeNewline
import io.provs.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.util.concurrent.TimeUnit
class RemoteProcessor(ip: String, 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()
}
}

@ -0,0 +1,34 @@
<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="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>
<root level="DEBUG">
<appender-ref ref="STDERR" />
<appender-ref ref="FILE" />
</root>
</configuration>

@ -0,0 +1,45 @@
package io.provs
import io.provs.processors.ContainerStartMode
import io.provs.processors.ContainerUbuntuHostProcessor
import io.provs.testconfig.tags.CONTAINERTEST
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Tag
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
@Tag(CONTAINERTEST)
fun cmd_works_with_echo() {
// given
val prov = Prov.newInstance(ContainerUbuntuHostProcessor("provs_test", startMode = ContainerStartMode.CREATE_NEW_KILL_EXISTING))
val text = "abc123!§$%&/#äöü"
// when
val res = prov.cmd("echo '${text}'")
// then
assert(res.success)
assertEquals(text + newline(), res.out)
}
@Test
@Tag(CONTAINERTEST)
fun cmdNoLog_works_with_echo() {
// given
val prov = Prov.newInstance(ContainerUbuntuHostProcessor("provs_test", startMode = ContainerStartMode.CREATE_NEW_KILL_EXISTING))
val text = "abc123!§$%&/#äöü"
// when
val res = prov.cmdNoLog("echo '${text}'")
// then
assert(res.success)
assertEquals(text + newline(), res.out)
}
}

@ -0,0 +1,91 @@
package io.provs
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)//"abc123!§\\$%&/\"\\äöü'")
}
@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)
}
@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())
}
}

@ -0,0 +1,458 @@
package io.provs
import io.provs.docker.provideContainer
import io.provs.testconfig.tags.CONTAINERTEST
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Tag
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)
@Tag(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.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 = Prov.defaultInstance().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 = Prov.defaultInstance().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 = Prov.defaultInstance().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 = Prov.defaultInstance().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 = Prov.defaultInstance().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 = Prov.defaultInstance().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 = Prov.defaultInstance().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 = Prov.defaultInstance().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 = Prov.defaultInstance().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 = Prov.defaultInstance().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
local().methodThatProvidesSomeOutput()
// then
System.setOut(originalOut)
System.setErr(originalErr)
println(outContent.toString())
// todo : simplify next lines
val expectedOutput = if (OS.WINDOWS.isCurrentOs) "\n" +
"============================================== SUMMARY (default 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()) {
"Processing started ...\n" +
".......processing completed.\n" +
"============================================== SUMMARY (default 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 = local().check("echo 123")
// then
assertTrue(res)
}
@Test
fun check_returnsFalse() {
// when
val res = local().check("cmddoesnotexist")
// then
assertFalse(res)
}
@Test
fun getSecret_returnsSecret() {
// when
val res = local().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 = local().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 = local().outer()
//then
assertEquals(ProvResult(false), res)
}
@Test
@EnabledOnOs(OS.LINUX)
@Tag(CONTAINERTEST)
fun inContainer_locally() {
// given
val containerName = "provs_testing"
local().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 = local().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)
}
}

@ -0,0 +1,28 @@
package io.provs
import io.provs.testconfig.tags.CONTAINERTEST
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
internal class UtilsTest {
@Test
fun test_getCallingMethodName() {
// when
val s = getCallingMethodName()
// then
Assertions.assertEquals("test_getCallingMethodName", s)
}
@Test
@Tag(CONTAINERTEST)
fun test_docker() {
// when
val res = docker().cmd("echo")
// then
Assertions.assertEquals(true, res.success)
}
}

@ -0,0 +1,33 @@
package io.provs.docker.platforms
import io.provs.ProvResult
import io.provs.docker.containerRuns
import io.provs.docker.exitAndRmContainer
import io.provs.docker.runContainer
import io.provs.local
import io.provs.testconfig.tags.CONTAINERTEST
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Tag
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)
@Tag(CONTAINERTEST)
fun runAndCheckAndExitContainer() {
// when
val containerName = "testContainer"
val result = local().requireAll {
runContainer(containerName)
addResultToEval(ProvResult(containerRuns(containerName)))
exitAndRmContainer(containerName)
}
// then
assertEquals(ProvResult(true), result)
}
}

@ -0,0 +1,18 @@
package io.provs.entry
import org.junit.jupiter.api.Test
@Suppress("unused")
fun test() {
println("test is fun")
}
internal class EntryKtTest {
@Test
fun test_main_no_arg() {
main("io.provs.entry.EntryTestKt", "test")
}
}

@ -0,0 +1,88 @@
package io.provs.platformTest
import io.provs.Prov
import io.provs.testconfig.tags.CONTAINERTEST
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.EnabledOnOs
import org.junit.jupiter.api.condition.OS
internal class UbuntuProvTests {
private val prov = Prov.defaultInstance()
private fun ping(url: String) = prov.def {
xec("ping", "-c", "4", url)
}
private fun outerPing() = prov.def {
ping("nu.nl")
}
@Test
@EnabledOnOs(OS.LINUX)
@Tag(CONTAINERTEST)
fun that_ping_works() {
// when
val res = outerPing()
// then
assert(res.success)
}
@Test
@EnabledOnOs(OS.LINUX)
fun that_cmd_works() {
// given
val a = Prov.defaultInstance()
// 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_nested_shells_work() {
// given
val a = Prov.defaultInstance()
// 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)
@Tag(CONTAINERTEST)
fun that_xec_works() {
// given
val a = Prov.defaultInstance()
// when
val res1 = a.xec("/usr/bin/printf", "hi")
val res2 = a.xec("/bin/ping", "-c", "2", "github.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")
}
}

@ -0,0 +1,37 @@
package io.provs.platformTest
import io.provs.getCallingMethodName
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 UbuntuProvUnitTest {
// @Test
// fun that_cond_executes_true_case() {
// // given
// val x = mockk<LocalProcessor>()
// every { x.x(*anyVararg()) } returns ProcessResult(0)
//
// val a = Prov.newInstance(x,"Linux")
//
// // when
// a.cond( { true }, { xec("doit") })
// a.cond( { false }, { xec("dont") })
//
// // then
// verify { x.x("doit") }
// verify(exactly = 0) { x.x("dont") }
// }
@Test
fun that_callingStack_works() {
// when
val s = getCallingMethodName()
// then
assert(s == "that_callingStack_works")
}
}

@ -0,0 +1,46 @@
package io.provs.platformTest
import io.provs.Prov
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 val prov = Prov.defaultInstance()
private fun ping(url: String) = prov.def {
cmd("ping $url")
}
private fun outerPing() = prov.def { ping("nu.nl") }
@Test
@EnabledOnOs(OS.WINDOWS)
fun def_definesPing_function() {
// when
val res = outerPing()
// then
assert(res.success)
}
@Test
@EnabledOnOs(OS.WINDOWS)
fun cmd_executesCommand() {
// given
val a = Prov.defaultInstance()
// 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())
}
}

@ -0,0 +1,24 @@
package io.provs.processors
import io.provs.platforms.SHELL
import io.provs.testconfig.tags.CONTAINERTEST
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.EnabledOnOs
import org.junit.jupiter.api.condition.OS.LINUX
internal class ContainerUbuntuHostProcessorTest {
@Test
@EnabledOnOs(LINUX)
@Tag(CONTAINERTEST)
fun test() {
if (System.getProperty("os.name") == "Linux") {
val processor = ContainerUbuntuHostProcessor("UbuntuHostContainerExecution", "ubuntu", ContainerStartMode.CREATE_NEW_KILL_EXISTING)
processor.installSudo()
processor.x(SHELL, "-c", "'cd /home && mkdir blabla'")
processor.exitAndRm()
}
}
}

@ -0,0 +1,3 @@
package io.provs.testconfig.tags
const val CONTAINERTEST = "containertest"

@ -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>
Loading…
Cancel
Save