Skip to content

Commit

Permalink
cfg4k-core: handle config file symlink changes
Browse files Browse the repository at this point in the history
  • Loading branch information
rocketraman authored and jdiazcano committed Mar 17, 2020
1 parent db1460b commit 3778db1
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.jdiazcano.cfg4k.reloadstrategies
import com.jdiazcano.cfg4k.providers.ConfigProvider
import java.io.File
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardWatchEventKinds
Expand All @@ -45,17 +46,33 @@ class FileChangeReloadStrategy(val file: Path) : ReloadStrategy {
}

override fun register(configProvider: ConfigProvider) {
val fileWatcher = file.toRealPath().parent.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY)
val parent = file.toRealPath().parent
val fileAbsolute = file.toRealPath()
// if its a symlink, then we should also watch symlinks for changes, this supports Kubernetes-style ConfigMap resources e.g.
// configfile -> ..data/configfile
// ..data -> ..2019_09_20_05_25_13.543205648
// ..2019_09_20_05_25_13.543205648/configfile
// Here, Kubernetes creates a new timestamped directory when the configmap changes, and just modifies the ..data symlink to point to it
// FileWatcher raises symlink changes (e.g. overwrite an existing link target on Linux via `ln -sfn`) as ENTRY_CREATE
// we don't use toRealPath() here, because we *want* the parent the file appears to be in, not the file's real parent
val fileWatcher = file.toAbsolutePath().parent.register(watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY)

watching = true
thread = thread(start = true, isDaemon = true) {
while (watching) {
try {
val key = watcher.take()
fileWatcher.pollEvents().forEach { event ->
if(event.kind() == StandardWatchEventKinds.OVERFLOW) return@forEach

val fileName = event.context() as Path
if (parent.resolve(fileName) == fileAbsolute) {
// if any entry in the chain of symbolic links leading to the actual file, including the actual
// file itself, has been created/modified, reload
val linkChain = generateSequence(file) {
if (Files.isSymbolicLink(it)) {
it.parent.resolve(Files.readSymbolicLink(it).first())
} else null
}.toList()

if (linkChain.contains(file.parent.resolve(fileName))) {
configProvider.reload()
}
}
Expand All @@ -74,4 +91,4 @@ class FileChangeReloadStrategy(val file: Path) : ReloadStrategy {
override fun deregister(configProvider: ConfigProvider) {
thread.interrupt()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ import com.jdiazcano.cfg4k.providers.get
import com.jdiazcano.cfg4k.sources.FileConfigSource
import io.kotlintest.shouldBe
import io.kotlintest.specs.FeatureSpec
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths

class FileChangeReloadStrategyTest : FeatureSpec({
feature("a provider with a filechange reloader") {
Expand Down Expand Up @@ -66,4 +64,55 @@ class FileChangeReloadStrategyTest : FeatureSpec({
provider.get<String>("foo").shouldBe("bar2") // We have canceled the reload so no more reloads
}
}
})

feature("a provider with a filechange reloader (with a symlink chain)") {
// simulate a Kubernetes ConfigMap setup
val tempDir = Files.createTempDirectory("cfg4k")
tempDir.toFile().let {
it.mkdirs()
it.deleteOnExit()
}

val numbered1 = Files.createDirectory(tempDir.resolve("..10001"))
val numbered2 = Files.createDirectory(tempDir.resolve("..10002"))
val dataLn = Files.createSymbolicLink(tempDir.resolve("..data"), tempDir.relativize(numbered1))

val file1 = numbered1.resolve("reloadedfile.properties").toFile()
file1.writeText("foo=bar")

val file2 = numbered2.resolve("reloadedfile.properties").toFile()
file2.writeText("foo=bar2")

val configFileLn = Files.createSymbolicLink(tempDir.resolve("reloadedfile.properties"),
tempDir.relativize(dataLn).resolve("reloadedfile.properties"))

val provider = DefaultConfigProvider(
PropertyConfigLoader(FileConfigSource(configFileLn)),
FileChangeReloadStrategy(configFileLn)
)

scenario("should have this config loaded") {
provider.get<String>("foo").shouldBe("bar")
}

scenario("should update the configuration when a soft link is updated") {
// change the ..data link like Kubernetes does
Files.delete(dataLn)
Files.createSymbolicLink(tempDir.resolve("..data"), tempDir.relativize(numbered2))

Thread.sleep(5000) // We need to wait a little bit for the event watcher to be triggered

provider.get<String>("foo").shouldBe("bar2")

provider.cancelReload()

// change the ..data link like Kubernetes does
Files.delete(dataLn)
Files.createSymbolicLink(tempDir.resolve("..data"), tempDir.relativize(numbered1))

Thread.sleep(5000) // We need to wait a little bit for the event watcher to be triggered

provider.get<String>("foo").shouldBe("bar2") // We have canceled the reload so no more reloads
}
}
})

0 comments on commit 3778db1

Please sign in to comment.