From ee658622b77c31637f145fc00dd31e00471c3546 Mon Sep 17 00:00:00 2001 From: ansgarz Date: Sat, 22 Jan 2022 19:38:07 +0100 Subject: [PATCH] add function resolve() for substitution of placeholders in Strings resp. files at runtime --- .../provs/framework/core/Utils.kt | 77 ++++++++++++++++--- .../ubuntu/filesystem/base/Filesystem.kt | 31 +++++++- .../provs/server/infrastructure/network.kt | 5 +- .../network/99-loopback.yaml.template | 2 +- .../provs/framework/core/UtilsKtTest.kt | 56 ++++++++++++++ .../nginx/ProvisionNginxKtTest.kt | 14 ++-- .../filesystem/base/FilesystemKtTest.kt | 19 +++++ .../framework/ubuntu/git/base/GitKtTest.kt | 1 - .../server/infrastructure/NetworkKtTest.kt | 34 ++++++++ 9 files changed, 217 insertions(+), 22 deletions(-) create mode 100644 src/test/kotlin/org/domaindrivenarchitecture/provs/server/infrastructure/NetworkKtTest.kt diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/Utils.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/Utils.kt index f807a28..ad38bdf 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/Utils.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/Utils.kt @@ -52,7 +52,15 @@ fun String.escapeBacktick(): String = replace("`", "\\`") fun String.escapeDollar(): String = replace("$", "\\$") fun String.escapeSingleQuoteForShell(): String = replace("'", "'\"'\"'") fun String.escapeProcentForPrintf(): String = replace("%", "%%") -fun String.endingWithFileSeparator(): String = if (length > 0 && (last() != fileSeparatorChar())) this + fileSeparator() else this +fun String.endingWithFileSeparator(): String = if (isNotEmpty() && (last() != fileSeparatorChar())) this + fileSeparator() else this + + +// -------------- Functions for system related properties ----------------- +fun fileSeparator(): String = File.separator +fun fileSeparatorChar(): Char = File.separatorChar +fun newline(): String = System.getProperty("line.separator") +fun hostUserHome(): String = System.getProperty("user.home") + fileSeparator() + /** * Put String between double quotes and escapes chars that need to be escaped (by backslash) for use in Unix Shell String @@ -82,12 +90,19 @@ internal fun echoCommandForTextWithNewlinesReplaced(text: String): String { } -fun fileSeparator(): String = File.separator -fun fileSeparatorChar(): Char = File.separatorChar -fun newline(): String = System.getProperty("line.separator") -fun hostUserHome(): String = System.getProperty("user.home") + fileSeparator() +/** + * Returns content of a local file as String (from the filesystem on the machine where provs has been initiated) + */ +internal fun getLocalFileContent(fullyQualifiedLocalFileName: String, sudo: Boolean = false): String { + val content = local().fileContent(fullyQualifiedLocalFileName, sudo) + check(content != null, { "Could not retrieve content from file: $fullyQualifiedLocalFileName" }) + return content +} +/** + * Returns content of a resource file as String + */ fun getResourceAsText(path: String): String { val resource = Thread.currentThread().contextClassLoader.getResource(path) requireNotNull(resource) { "Resource $path not found" } @@ -95,13 +110,55 @@ fun getResourceAsText(path: String): String { } -internal fun getLocalFileContent(fullyQualifiedLocalFileName: String, sudo: Boolean = false): String { - val content = local().fileContent(fullyQualifiedLocalFileName, sudo) - check(content != null, { "Could not retrieve content from file: $fullyQualifiedLocalFileName" }) - return content +/** + * Returns content of a resource file as String with the variables resolved + */ +fun getResourceResolved(path: String, values: Map): String { + val resource = Thread.currentThread().contextClassLoader.getResource(path) + requireNotNull(resource) { "Resource $path not found" } + return resource.readText().resolve(values) } +/** + * Returns a String in which placeholders (e.g. $var or ${var}) are replaced by the specified values. + * This function can be used for resolving templates at RUNTIME (e.g. for templates read from files) as + * for compile time this functionality is already provided by the compiler out-of-the-box, of course. + * For a usage example see the corresponding test. + */ +fun String.resolve( + values: Map +): String { + var text = this + + // replace all simple variable patterns (i.e. without curly braces) + val matcherSimple = Regex("\\$([a-zA-Z_][a-zA-Z_0-9]*)") + var match = matcherSimple.find(text) + while (match != null) { + val variableName = match.groupValues.get(1) + val newText = values.get(variableName) + require(newText != null, { "No value found for: " + variableName }) + text = text.replace("\$$variableName", newText) + match = matcherSimple.find(text) + } + + // replace all variables within curly braces + val matcherWithBraces = Regex("\\$\\{([a-zA-Z_][a-zA-Z_0-9]*)}") + match = matcherWithBraces.find(text) + while (match != null) { + val variableName = match.groupValues.get(1) + val newText = values.get(variableName) + require(newText != null, { "No value found for: " + variableName }) + text = text.replace("\${$variableName}", newText) + match = matcherWithBraces.find(text) + } + + // replace escaped dollars + text = text.replace("\${'$'}", "\$") + + return text +} + /** * Returns default local Prov instance. */ @@ -126,7 +183,7 @@ fun remote(host: String, remoteUser: String, password: Secret? = null, platform: /** - * Returns Prov instance which eexcutes its tasks in a local docker container with name containerName. + * Returns Prov instance which executes its tasks in a local docker container with name containerName. * If a container with the given name is running already, it'll be reused if parameter useExistingContainer is set to true. * If a container is reused, it is not checked if it has the correct, specified image. * Determines automatically if sudo is required if sudo is null, otherwise the specified sudo is used diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/filesystem/base/Filesystem.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/filesystem/base/Filesystem.kt index d4f1de3..31a76b7 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/filesystem/base/Filesystem.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/filesystem/base/Filesystem.kt @@ -6,11 +6,17 @@ import org.domaindrivenarchitecture.provs.framework.core.getLocalFileContent import java.io.File +/** + * Returns true if the given file exists. + */ fun Prov.fileExists(file: String, sudo: Boolean = false): Boolean { return cmdNoEval((if (sudo) "sudo " else "") + "test -e " + file).success } +/** + * Creates a file with its content retrieved from a local resource file + */ fun Prov.createFileFromResource( fullyQualifiedFilename: String, resourceFilename: String, @@ -27,6 +33,26 @@ fun Prov.createFileFromResource( } +/** + * Creates a file with its content retrieved of a local resource file in which placeholders are substituted with the specified values. + */ +fun Prov.createFileFromResourceTemplate( + fullyQualifiedFilename: String, + resourceFilename: String, + resourcePath: String = "", + values: Map, + posixFilePermission: String? = null, + sudo: Boolean = false +): ProvResult = def { + createFile( + fullyQualifiedFilename, + getResourceAsText(resourcePath.endingWithFileSeparator() + resourceFilename).resolve(values), + posixFilePermission, + sudo + ) +} + + /** * Copies a file from the local environment to the running Prov instance. * In case the running ProvInstance is also local, it would copy from local to local. @@ -46,6 +72,9 @@ fun Prov.copyFileFromLocal( } +/** + * Creates a file with the specified data. If text is null, an empty file is created + */ fun Prov.createFile( fullyQualifiedFilename: String, text: String?, @@ -96,7 +125,7 @@ fun Prov.deleteFile(file: String, path: String? = null, sudo: Boolean = false): fun Prov.fileContainsText(file: String, content: String, sudo: Boolean = false): Boolean { - return cmdNoEval((if (sudo) "sudo " else "") + "grep '${content.escapeSingleQuote()}' $file").success + return cmdNoEval((if (sudo) "sudo " else "") + "grep -- '${content.escapeSingleQuote()}' $file").success } diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/server/infrastructure/network.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/server/infrastructure/network.kt index 5e6a32b..6cdb57b 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/server/infrastructure/network.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/server/infrastructure/network.kt @@ -2,7 +2,7 @@ package org.domaindrivenarchitecture.provs.server.infrastructure import org.domaindrivenarchitecture.provs.framework.core.Prov import org.domaindrivenarchitecture.provs.framework.core.ProvResult -import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFileFromResource +import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFileFromResourceTemplate import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.fileExists val loopbackFile = "/etc/netplan/99-loopback.yaml" @@ -14,10 +14,11 @@ fun Prov.testNetworkExists(): Boolean { fun Prov.provisionNetwork() = task { if(!testNetworkExists()) { - createFileFromResource( + createFileFromResourceTemplate( loopbackFile, "99-loopback.yaml.template", resourcePath, + mapOf("ip" to "192.168.5.1/32"), "644", sudo = true ) diff --git a/src/main/resources/org/domaindrivenarchitecture/provs/infrastructure/network/99-loopback.yaml.template b/src/main/resources/org/domaindrivenarchitecture/provs/infrastructure/network/99-loopback.yaml.template index a349685..31314d9 100644 --- a/src/main/resources/org/domaindrivenarchitecture/provs/infrastructure/network/99-loopback.yaml.template +++ b/src/main/resources/org/domaindrivenarchitecture/provs/infrastructure/network/99-loopback.yaml.template @@ -6,4 +6,4 @@ network: match: name: lo addresses: - - 192.168.5.1/32 + - $ip diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/core/UtilsKtTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/core/UtilsKtTest.kt index 34c8e18..70ed63a 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/core/UtilsKtTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/core/UtilsKtTest.kt @@ -66,4 +66,60 @@ internal class UtilsKtTest { fun test_remote() { assertTrue(remote("127.0.0.1", "user").cmd("echo sth").success) } + + @Test + fun test_resolveTemplate_successfully() { + // given + val DOUBLE_ESCAPED_DOLLAR = "\${'\${'\$'}'}" + val input = """ + line1 + line2: ${'$'}var1 + line3 + line4=${'$'}var2 + line5 with 3 dollars ${'$'}${'$'}${'$'} + line6${'$'}{var3}withpostfix + line7 with double escaped dollars ${DOUBLE_ESCAPED_DOLLAR} + """.trimIndent() + + // when + val res = input.resolve(values = mapOf("var1" to "VALUE1", "var2" to "VALUE2", "var3" to "VALUE3")) + + // then + val ESCAPED_DOLLAR = "\${'\$'}" + val RESULT = """ + line1 + line2: VALUE1 + line3 + line4=VALUE2 + line5 with 3 dollars ${'$'}${'$'}${'$'} + line6VALUE3withpostfix + line7 with double escaped dollars $ESCAPED_DOLLAR + """.trimIndent() + + assertEquals(RESULT, res) + } + + + @Test + fun test_resolveTemplate_with_invalid_data_throws_exception() { + // given + val DOUBLE_ESCAPED_DOLLAR = "\${'\${'\$'}'}" + val input = """ + line1 + line2: ${'$'}var1 + line3 + line4=${'$'}var2 + line5 with 3 dollars ${'$'}${'$'}${'$'} + line6${'$'}{var3}withpostfix + line7 with double escaped dollars ${DOUBLE_ESCAPED_DOLLAR} + """.trimIndent() + + // when + val e = assertThrows(IllegalArgumentException::class.java) { + input.resolve(values = mapOf("var1" to "VALUE1", "var2" to "VALUE2", "wrongkey" to "VALUE3")) + } + + // then + assertEquals(e.message, "No value found for: var3") + } } \ No newline at end of file diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/extensions/server_software/standalone_server/nginx/ProvisionNginxKtTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/extensions/server_software/standalone_server/nginx/ProvisionNginxKtTest.kt index 70e2317..8aa8d66 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/extensions/server_software/standalone_server/nginx/ProvisionNginxKtTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/extensions/server_software/standalone_server/nginx/ProvisionNginxKtTest.kt @@ -1,13 +1,13 @@ package org.domaindrivenarchitecture.provs.framework.extensions.server_software.standalone_server.nginx -import org.domaindrivenarchitecture.provs.test.defaultTestContainer -import org.domaindrivenarchitecture.provs.test.tags.NonCi +import org.domaindrivenarchitecture.provs.framework.extensions.server_software.standalone_server.nginx.base.* +import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.fileExists import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.replaceTextInFile import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall -import org.domaindrivenarchitecture.provs.framework.extensions.server_software.standalone_server.nginx.base.* +import org.domaindrivenarchitecture.provs.test.defaultTestContainer import org.domaindrivenarchitecture.provs.test.tags.ContainerTest -import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.fileExists import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test @@ -15,7 +15,7 @@ internal class ProvisionNginxKtTest { @Test @ContainerTest - @NonCi + @Disabled // Not running on (unprivileged ??) container fun provisionNginxStandAlone_customConfig() { // given val a = defaultTestContainer() @@ -45,7 +45,7 @@ internal class ProvisionNginxKtTest { @Test @ContainerTest - @NonCi + @Disabled // Not running on (unprivileged ??) container fun provisionNginxStandAlone_defaultConfig() { // given val a = defaultTestContainer() @@ -61,7 +61,7 @@ internal class ProvisionNginxKtTest { @Test @ContainerTest - @NonCi + @Disabled // Not running on (unprivileged ??) container fun provisionNginxStandAlone_sslConfig() { // given val a = defaultTestContainer() diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/filesystem/base/FilesystemKtTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/filesystem/base/FilesystemKtTest.kt index 4952862..36e6d45 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/filesystem/base/FilesystemKtTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/filesystem/base/FilesystemKtTest.kt @@ -207,4 +207,23 @@ internal class FilesystemKtTest { val content = defaultTestContainer().fileContent( "copiedFileFromLocal") assertEquals("resource text\n", content) } + + @Test + @ContainerTest + fun test_fileContainsText() { + // given + defaultTestContainer().createFile("testfilecontainingtext", "abc\n- def\nefg") + + // when + val res = defaultTestContainer().fileContainsText("testfilecontainingtext", "abc") + val res2 = defaultTestContainer().fileContainsText("testfilecontainingtext", "de") + val res3 = defaultTestContainer().fileContainsText("testfilecontainingtext", "- def") + val res4 = defaultTestContainer().fileContainsText("testfilecontainingtext", "xyy") + + // then + assertTrue(res) + assertTrue(res2) + assertTrue(res3) + assertFalse(res4) + } } \ No newline at end of file diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/git/base/GitKtTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/git/base/GitKtTest.kt index 29fc01b..1de2758 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/git/base/GitKtTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/git/base/GitKtTest.kt @@ -38,7 +38,6 @@ internal class GitKtTest { // when prov.trustGithub() - prov.gitClone("https://github.com/DomainDrivenArchitecture/dda-git-crate.git", "~/") val res = prov.gitClone("https://github.com/DomainDrivenArchitecture/dda-git-crate.git", "~/") // then diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/server/infrastructure/NetworkKtTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/server/infrastructure/NetworkKtTest.kt new file mode 100644 index 0000000..14831ee --- /dev/null +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/server/infrastructure/NetworkKtTest.kt @@ -0,0 +1,34 @@ +package org.domaindrivenarchitecture.provs.server.infrastructure + +import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createDirs +import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.fileContainsText +import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall +import org.domaindrivenarchitecture.provs.test.defaultTestContainer +import org.domaindrivenarchitecture.provs.test.tags.ContainerTest +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.Assertions.* + +internal class NetworkKtTest { + + @Test + @ContainerTest + fun test_provisionNetwork() { + // given + val p = defaultTestContainer() + p.task { + aptInstall("dbus netplan.io") + createDirs("/etc/netplan", sudo = true) + cmd("/etc/init.d/dbus start", sudo = true) + } + + // when + val res = p.provisionNetwork() + + // then + // assertTrue(res.success) -- netplan is not working in an unprivileged container - see also https://askubuntu.com/questions/813588/systemctl-failed-to-connect-to-bus-docker-ubuntu16-04-container + + // check file content snippet + assertTrue(p.fileContainsText("/etc/netplan/99-loopback.yaml", content = "- 192.168.5.1/32", sudo = true)) + } +} \ No newline at end of file