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.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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,4 +6,4 @@ network:
|
||||||
match:
|
match:
|
||||||
name: lo
|
name: lo
|
||||||
addresses:
|
addresses:
|
||||||
- 192.168.5.1/32
|
- $ip
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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