diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/FileWatcher.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/FileWatcher.java index 449dcccabd2..ea23fed472d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/FileWatcher.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/FileWatcher.java @@ -28,6 +28,7 @@ import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.time.Duration; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -85,11 +86,7 @@ class FileWatcher implements Closeable { this.thread = new WatcherThread(); this.thread.start(); } - Set registrationPaths = new HashSet<>(); - for (Path path : paths) { - registrationPaths.addAll(getRegistrationPaths(path)); - } - this.thread.register(new Registration(registrationPaths, action)); + this.thread.register(new Registration(getRegistrationPaths(paths), action)); } catch (IOException ex) { throw new UncheckedIOException("Failed to register paths for watching: " + paths, ex); @@ -97,6 +94,52 @@ class FileWatcher implements Closeable { } } + /** + * Retrieves all {@link Path Paths} that should be registered for the specified + * {@link Path}. If the path is a symlink, changes to the symlink should be monitored, + * not just the file it points to. For example, for the given {@code keystore.jks} + * path in the following directory structure:
+	 * +- stores
+	 * |  +─ keystore.jks
+	 * +- data -> stores
+	 * +─ keystore.jks -> data/keystore.jks
+	 * 
the resulting paths would include: + *

+ *

+ * @param paths the source paths + * @return all possible {@link Path} instances to be registered + * @throws IOException if an I/O error occurs + */ + private Set getRegistrationPaths(Set paths) throws IOException { + Set result = new HashSet<>(); + for (Path path : paths) { + collectRegistrationPaths(path, result); + } + return Collections.unmodifiableSet(result); + } + + private void collectRegistrationPaths(Path path, Set result) throws IOException { + path = path.toAbsolutePath(); + result.add(path); + Path parent = path.getParent(); + if (parent != null && Files.isSymbolicLink(parent)) { + result.add(parent); + collectRegistrationPaths(resolveSiblingSymbolicLink(parent).resolve(path.getFileName()), result); + } + else if (Files.isSymbolicLink(path)) { + collectRegistrationPaths(resolveSiblingSymbolicLink(path), result); + } + } + + private Path resolveSiblingSymbolicLink(Path path) throws IOException { + return path.resolveSibling(Files.readSymbolicLink(path)); + } + @Override public void close() throws IOException { synchronized (this.lock) { @@ -114,44 +157,6 @@ class FileWatcher implements Closeable { } } - /** - * Retrieves all {@link Path Paths} that should be registered for the specified - * {@link Path}. If the path is a symlink, changes to the symlink should be monitored, - * not just the file it points to. For example, for the given {@code keystore.jks} - * path in the following directory structure:
-	 * .
-	 * ├── ..a72e81ff-f0e1-41d8-a19b-068d3d1d4e2f
-	 * │   ├── keystore.jks
-	 * ├── ..data -> ..a72e81ff-f0e1-41d8-a19b-068d3d1d4e2f
-	 * ├── keystore.jks -> ..data/keystore.jks
-	 * 
the resulting paths would include: - *
    - *
  • keystore.jks
  • - *
  • ..data/keystore.jks
  • - *
  • ..data
  • - *
  • ..a72e81ff-f0e1-41d8-a19b-068d3d1d4e2f/keystore.jks
  • - *
- * @param path the path - * @return all possible {@link Path} instances to be registered - * @throws IOException if an I/O error occurs - */ - private static Set getRegistrationPaths(Path path) throws IOException { - path = path.toAbsolutePath(); - Set result = new HashSet<>(); - result.add(path); - Path parent = path.getParent(); - if (parent != null && Files.isSymbolicLink(parent)) { - result.add(parent); - Path target = parent.resolveSibling(Files.readSymbolicLink(parent)); - result.addAll(getRegistrationPaths(target.resolve(path.getFileName()))); - } - else if (Files.isSymbolicLink(path)) { - Path target = path.resolveSibling(Files.readSymbolicLink(path)); - result.addAll(getRegistrationPaths(target)); - } - return result; - } - /** * The watcher thread used to check for changes. */ @@ -254,6 +259,9 @@ class FileWatcher implements Closeable { /** * An individual watch registration. + * + * @param paths the paths being registered + * @param action the action to take */ private record Registration(Set paths, Runnable action) { @@ -265,6 +273,7 @@ class FileWatcher implements Closeable { private boolean isInDirectories(Path file) { return this.paths.stream().filter(Files::isDirectory).anyMatch(file::startsWith); } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/FileWatcherTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/FileWatcherTests.java index 07d651b1a75..11501206ac9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/FileWatcherTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/FileWatcherTests.java @@ -259,24 +259,24 @@ class FileWatcherTests { /** * Updates many times K8s ConfigMap/Secret with atomic move.
 	 * .
-	 * ├── ..a72e81ff-f0e1-41d8-a19b-068d3d1d4e2f
-	 * │   ├── keystore.jks
-	 * ├── ..data -> ..a72e81ff-f0e1-41d8-a19b-068d3d1d4e2f
-	 * ├── keystore.jks -> ..data/keystore.jks
+	 * +─ ..a72e81ff-f0e1-41d8-a19b-068d3d1d4e2f
+	 * │  +─ keystore.jks
+	 * +─ ..data -> ..a72e81ff-f0e1-41d8-a19b-068d3d1d4e2f
+	 * +─ keystore.jks -> ..data/keystore.jks
 	 * 
* * After a first a ConfigMap/Secret update, this will look like:
 	 * .
-	 * ├── ..bba2a61f-ce04-4c35-93aa-e455110d4487
-	 * │   ├── keystore.jks
-	 * ├── ..data -> ..bba2a61f-ce04-4c35-93aa-e455110d4487
-	 * ├── keystore.jks -> ..data/keystore.jks
+	 * +─ ..bba2a61f-ce04-4c35-93aa-e455110d4487
+	 * │  +─ keystore.jks
+	 * +─ ..data -> ..bba2a61f-ce04-4c35-93aa-e455110d4487
+	 * +─ keystore.jks -> ..data/keystore.jks
 	 * 
After a second a ConfigMap/Secret update, this will look like:
 	 * .
-	 * ├── ..134887f0-df8f-4433-b70c-7784d2a33bd1
-	 * │   ├── keystore.jks
-	 * ├── ..data -> ..134887f0-df8f-4433-b70c-7784d2a33bd1
-	 * ├── keystore.jks -> ..data/keystore.jks
+	 * +─ ..134887f0-df8f-4433-b70c-7784d2a33bd1
+	 * │  +─ keystore.jks
+	 * +─ ..data -> ..134887f0-df8f-4433-b70c-7784d2a33bd1
+	 * +─ keystore.jks -> ..data/keystore.jks
 	 *
*

* When Kubernetes updates either the ConfigMap or Secret, it performs the following