add function resolve() for substitution of placeholders in Strings resp. files at runtime
This commit is contained in:
parent
58eaf14f0e
commit
ee658622b7
9 changed files with 217 additions and 22 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -6,4 +6,4 @@ network:
|
|||
match:
|
||||
name: lo
|
||||
addresses:
|
||||
- 192.168.5.1/32
|
||||
- $ip
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue