diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b1d073a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.gradle/* +/build/* +/out/* +/.idea/* +*.log diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..18048da --- /dev/null +++ b/.gitlab-ci.yml @@ -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 ----------" \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..d5e12ab --- /dev/null +++ b/Readme.md @@ -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. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..9d8d5cb --- /dev/null +++ b/build.gradle @@ -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) + } + } + } + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..744aa7f --- /dev/null +++ b/gradle.properties @@ -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 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..5c2d1cf Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..12d38de --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..b0d6d0a --- /dev/null +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9991c50 --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..9f91a65 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'provs-core' diff --git a/src/main/kotlin/io/provs/Prov.kt b/src/main/kotlin/io/provs/Prov.kt new file mode 100644 index 0000000..20f54a2 --- /dev/null +++ b/src/main/kotlin/io/provs/Prov.kt @@ -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() + 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 { + 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.") + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/provs/Results.kt b/src/main/kotlin/io/provs/Results.kt new file mode 100644 index 0000000..8c32c1d --- /dev/null +++ b/src/main/kotlin/io/provs/Results.kt @@ -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(val success: Boolean, val resultObject: T? = null) { + override fun toString(): String { + return "TypedResult:: ${if (success) "Succeeded" else "FAILED"} -- Result object: " + resultObject?.run { toString().escapeNewline() } + } +} diff --git a/src/main/kotlin/io/provs/Secret.kt b/src/main/kotlin/io/provs/Secret.kt new file mode 100644 index 0000000..e9eceeb --- /dev/null +++ b/src/main/kotlin/io/provs/Secret.kt @@ -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) \ No newline at end of file diff --git a/src/main/kotlin/io/provs/Utils.kt b/src/main/kotlin/io/provs/Utils.kt new file mode 100644 index 0000000..cc2e6ef --- /dev/null +++ b/src/main/kotlin/io/provs/Utils.kt @@ -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) + } +} diff --git a/src/main/kotlin/io/provs/docker/HostDocker.kt b/src/main/kotlin/io/provs/docker/HostDocker.kt new file mode 100644 index 0000000..38bcd76 --- /dev/null +++ b/src/main/kotlin/io/provs/docker/HostDocker.kt @@ -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) + } +} diff --git a/src/main/kotlin/io/provs/docker/images/DockerImages.kt b/src/main/kotlin/io/provs/docker/images/DockerImages.kt new file mode 100644 index 0000000..1fe8423 --- /dev/null +++ b/src/main/kotlin/io/provs/docker/images/DockerImages.kt @@ -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 +""" + } +} diff --git a/src/main/kotlin/io/provs/docker/platforms/UbuntuHostDocker.kt b/src/main/kotlin/io/provs/docker/platforms/UbuntuHostDocker.kt new file mode 100644 index 0000000..84c0b57 --- /dev/null +++ b/src/main/kotlin/io/provs/docker/platforms/UbuntuHostDocker.kt @@ -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") +} diff --git a/src/main/kotlin/io/provs/entry/Entry.kt b/src/main/kotlin/io/provs/entry/Entry.kt new file mode 100644 index 0000000..7655107 --- /dev/null +++ b/src/main/kotlin/io/provs/entry/Entry.kt @@ -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>( + Array::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()) + } else if (args.size == 2) { + method.invoke(null) + } else { + method.invoke(null, args.drop(2).toTypedArray()) + } + } else { + println("Usage: ") + } +} diff --git a/src/main/kotlin/io/provs/platforms/UbuntuProv.kt b/src/main/kotlin/io/provs/platforms/UbuntuProv.kt new file mode 100644 index 0000000..c4e4f34 --- /dev/null +++ b/src/main/kotlin/io/provs/platforms/UbuntuProv.kt @@ -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") + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/provs/platforms/WinProv.kt b/src/main/kotlin/io/provs/platforms/WinProv.kt new file mode 100644 index 0000000..f8a5703 --- /dev/null +++ b/src/main/kotlin/io/provs/platforms/WinProv.kt @@ -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") + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/provs/processors/ContainerUbuntuHostProcessor.kt b/src/main/kotlin/io/provs/processors/ContainerUbuntuHostProcessor.kt new file mode 100644 index 0000000..2c3f8ea --- /dev/null +++ b/src/main/kotlin/io/provs/processors/ContainerUbuntuHostProcessor.kt @@ -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() + } + } +} diff --git a/src/main/kotlin/io/provs/processors/LocalProcessor.kt b/src/main/kotlin/io/provs/processors/LocalProcessor.kt new file mode 100644 index 0000000..d0f8505 --- /dev/null +++ b/src/main/kotlin/io/provs/processors/LocalProcessor.kt @@ -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) + } + } +} diff --git a/src/main/kotlin/io/provs/processors/PrintOnlyProcessor.kt b/src/main/kotlin/io/provs/processors/PrintOnlyProcessor.kt new file mode 100644 index 0000000..4a6fb2c --- /dev/null +++ b/src/main/kotlin/io/provs/processors/PrintOnlyProcessor.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/provs/processors/Processor.kt b/src/main/kotlin/io/provs/processors/Processor.kt new file mode 100644 index 0000000..e13b31f --- /dev/null +++ b/src/main/kotlin/io/provs/processors/Processor.kt @@ -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 = emptyArray()) { + + private fun success(): Boolean { + return (exitCode == 0) + } + + fun argsToString() : String { + return args.joinToString( + separator = ", ", + prefix = "[", + postfix = "]", + limit = 4, + truncated = " ..." + ) + } + + override fun toString(): String { + return "--->>> ProcessResult: ${if (success()) "Succeeded" else "FAILED"} -- Code: $exitCode, ${if (!out.isNullOrEmpty()) "Out: $out, " else ""}${if (!err.isNullOrEmpty()) "Err: $err" else ""}" + argsToString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ProcessResult + + if (exitCode != other.exitCode) return false + if (out != other.out) return false + if (err != other.err) return false + if (ex != other.ex) return false + if (!args.contentEquals(other.args)) return false + + return true + } + + override fun hashCode(): Int { + var result = exitCode + result = 31 * result + (out?.hashCode() ?: 0) + result = 31 * result + (err?.hashCode() ?: 0) + result = 31 * result + ex.hashCode() + result = 31 * result + args.contentHashCode() + return result + } +} diff --git a/src/main/kotlin/io/provs/processors/RemoteUbuntuProcessor.kt b/src/main/kotlin/io/provs/processors/RemoteUbuntuProcessor.kt new file mode 100644 index 0000000..0e93cfe --- /dev/null +++ b/src/main/kotlin/io/provs/processors/RemoteUbuntuProcessor.kt @@ -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() + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..5c96bb0 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,34 @@ + + + + + + System.err + + WARN + + + %d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{35}) - %msg %n + + + + + ./logs/provs-${byTime}.log + + ./logs/provs-%d{yyyy-MM-dd}.%i.log + 10MB + 3 + 1GB + + true + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + \ No newline at end of file diff --git a/src/test/kotlin/io/provs/ContainerProcessorTest.kt b/src/test/kotlin/io/provs/ContainerProcessorTest.kt new file mode 100644 index 0000000..394a2e3 --- /dev/null +++ b/src/test/kotlin/io/provs/ContainerProcessorTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/LocalProcessorTest.kt b/src/test/kotlin/io/provs/LocalProcessorTest.kt new file mode 100644 index 0000000..c769c4e --- /dev/null +++ b/src/test/kotlin/io/provs/LocalProcessorTest.kt @@ -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()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/ProvTest.kt b/src/test/kotlin/io/provs/ProvTest.kt new file mode 100644 index 0000000..2299fb3 --- /dev/null +++ b/src/test/kotlin/io/provs/ProvTest.kt @@ -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) + } +} diff --git a/src/test/kotlin/io/provs/UtilsTest.kt b/src/test/kotlin/io/provs/UtilsTest.kt new file mode 100644 index 0000000..1309b1c --- /dev/null +++ b/src/test/kotlin/io/provs/UtilsTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/docker/platforms/UbuntuHostDockerKtTest.kt b/src/test/kotlin/io/provs/docker/platforms/UbuntuHostDockerKtTest.kt new file mode 100644 index 0000000..16d4946 --- /dev/null +++ b/src/test/kotlin/io/provs/docker/platforms/UbuntuHostDockerKtTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/entry/EntryTest.kt b/src/test/kotlin/io/provs/entry/EntryTest.kt new file mode 100644 index 0000000..68dc6e8 --- /dev/null +++ b/src/test/kotlin/io/provs/entry/EntryTest.kt @@ -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") + } +} diff --git a/src/test/kotlin/io/provs/platformTest/UbuntuProvTests.kt b/src/test/kotlin/io/provs/platformTest/UbuntuProvTests.kt new file mode 100644 index 0000000..8e143ec --- /dev/null +++ b/src/test/kotlin/io/provs/platformTest/UbuntuProvTests.kt @@ -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") + } + +} + diff --git a/src/test/kotlin/io/provs/platformTest/UbuntuProvUnitTest.kt b/src/test/kotlin/io/provs/platformTest/UbuntuProvUnitTest.kt new file mode 100644 index 0000000..92fe7b3 --- /dev/null +++ b/src/test/kotlin/io/provs/platformTest/UbuntuProvUnitTest.kt @@ -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() +// 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") + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/platformTest/WinProvTests.kt b/src/test/kotlin/io/provs/platformTest/WinProvTests.kt new file mode 100644 index 0000000..1e10248 --- /dev/null +++ b/src/test/kotlin/io/provs/platformTest/WinProvTests.kt @@ -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()) + } +} + diff --git a/src/test/kotlin/io/provs/processors/ContainerUbuntuHostProcessorTest.kt b/src/test/kotlin/io/provs/processors/ContainerUbuntuHostProcessorTest.kt new file mode 100644 index 0000000..57dc8df --- /dev/null +++ b/src/test/kotlin/io/provs/processors/ContainerUbuntuHostProcessorTest.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/testconfig/tags/Tags.kt b/src/test/kotlin/io/provs/testconfig/tags/Tags.kt new file mode 100644 index 0000000..b40abc2 --- /dev/null +++ b/src/test/kotlin/io/provs/testconfig/tags/Tags.kt @@ -0,0 +1,3 @@ +package io.provs.testconfig.tags + +const val CONTAINERTEST = "containertest" \ No newline at end of file diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 0000000..a43a2a2 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,19 @@ + + + + + + System.err + + + %d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{35}) - %msg %n + + + + + + + + + + \ No newline at end of file