add function resolve() for substitution of placeholders in Strings resp. files at runtime

This commit is contained in:
ansgarz 2022-01-22 19:38:07 +01:00
parent 58eaf14f0e
commit ee658622b7
9 changed files with 217 additions and 22 deletions

View file

@ -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, String>): 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, String>
): 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

View file

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

View file

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

View file

@ -6,4 +6,4 @@ network:
match:
name: lo
addresses:
- 192.168.5.1/32
- $ip

View file

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

View file

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

View file

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

View file

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

View file

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