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.escapeDollar(): String = replace("$", "\\$")
fun String.escapeSingleQuoteForShell(): String = replace("'", "'\"'\"'") fun String.escapeSingleQuoteForShell(): String = replace("'", "'\"'\"'")
fun String.escapeProcentForPrintf(): 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 * 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 * Returns content of a local file as String (from the filesystem on the machine where provs has been initiated)
fun newline(): String = System.getProperty("line.separator") */
fun hostUserHome(): String = System.getProperty("user.home") + fileSeparator() 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 { fun getResourceAsText(path: String): String {
val resource = Thread.currentThread().contextClassLoader.getResource(path) val resource = Thread.currentThread().contextClassLoader.getResource(path)
requireNotNull(resource) { "Resource $path not found" } 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) * Returns content of a resource file as String with the variables resolved
check(content != null, { "Could not retrieve content from file: $fullyQualifiedLocalFileName" }) */
return content 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. * 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 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. * 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 * 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 import java.io.File
/**
* Returns true if the given file exists.
*/
fun Prov.fileExists(file: String, sudo: Boolean = false): Boolean { fun Prov.fileExists(file: String, sudo: Boolean = false): Boolean {
return cmdNoEval((if (sudo) "sudo " else "") + "test -e " + file).success 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( fun Prov.createFileFromResource(
fullyQualifiedFilename: String, fullyQualifiedFilename: String,
resourceFilename: 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. * 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. * 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( fun Prov.createFile(
fullyQualifiedFilename: String, fullyQualifiedFilename: String,
text: 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 { 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.Prov
import org.domaindrivenarchitecture.provs.framework.core.ProvResult 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 import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.fileExists
val loopbackFile = "/etc/netplan/99-loopback.yaml" val loopbackFile = "/etc/netplan/99-loopback.yaml"
@ -14,10 +14,11 @@ fun Prov.testNetworkExists(): Boolean {
fun Prov.provisionNetwork() = task { fun Prov.provisionNetwork() = task {
if(!testNetworkExists()) { if(!testNetworkExists()) {
createFileFromResource( createFileFromResourceTemplate(
loopbackFile, loopbackFile,
"99-loopback.yaml.template", "99-loopback.yaml.template",
resourcePath, resourcePath,
mapOf("ip" to "192.168.5.1/32"),
"644", "644",
sudo = true sudo = true
) )

View file

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

View file

@ -66,4 +66,60 @@ internal class UtilsKtTest {
fun test_remote() { fun test_remote() {
assertTrue(remote("127.0.0.1", "user").cmd("echo sth").success) 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 package org.domaindrivenarchitecture.provs.framework.extensions.server_software.standalone_server.nginx
import org.domaindrivenarchitecture.provs.test.defaultTestContainer import org.domaindrivenarchitecture.provs.framework.extensions.server_software.standalone_server.nginx.base.*
import org.domaindrivenarchitecture.provs.test.tags.NonCi import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.fileExists
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.replaceTextInFile import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.replaceTextInFile
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall 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.test.tags.ContainerTest
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.fileExists
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -15,7 +15,7 @@ internal class ProvisionNginxKtTest {
@Test @Test
@ContainerTest @ContainerTest
@NonCi @Disabled // Not running on (unprivileged ??) container
fun provisionNginxStandAlone_customConfig() { fun provisionNginxStandAlone_customConfig() {
// given // given
val a = defaultTestContainer() val a = defaultTestContainer()
@ -45,7 +45,7 @@ internal class ProvisionNginxKtTest {
@Test @Test
@ContainerTest @ContainerTest
@NonCi @Disabled // Not running on (unprivileged ??) container
fun provisionNginxStandAlone_defaultConfig() { fun provisionNginxStandAlone_defaultConfig() {
// given // given
val a = defaultTestContainer() val a = defaultTestContainer()
@ -61,7 +61,7 @@ internal class ProvisionNginxKtTest {
@Test @Test
@ContainerTest @ContainerTest
@NonCi @Disabled // Not running on (unprivileged ??) container
fun provisionNginxStandAlone_sslConfig() { fun provisionNginxStandAlone_sslConfig() {
// given // given
val a = defaultTestContainer() val a = defaultTestContainer()

View file

@ -207,4 +207,23 @@ internal class FilesystemKtTest {
val content = defaultTestContainer().fileContent( "copiedFileFromLocal") val content = defaultTestContainer().fileContent( "copiedFileFromLocal")
assertEquals("resource text\n", content) 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 // when
prov.trustGithub() prov.trustGithub()
prov.gitClone("https://github.com/DomainDrivenArchitecture/dda-git-crate.git", "~/")
val res = prov.gitClone("https://github.com/DomainDrivenArchitecture/dda-git-crate.git", "~/") val res = prov.gitClone("https://github.com/DomainDrivenArchitecture/dda-git-crate.git", "~/")
// then // 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))
}
}