|
1 | 1 | /*
|
2 |
| - * Copyright 2012-2024 the original author or authors. |
| 2 | + * Copyright 2012-2025 the original author or authors. |
3 | 3 | *
|
4 | 4 | * Licensed under the Apache License, Version 2.0 (the "License");
|
5 | 5 | * you may not use this file except in compliance with the License.
|
|
31 | 31 | import org.junit.jupiter.api.Test;
|
32 | 32 | import org.junit.jupiter.api.io.TempDir;
|
33 | 33 |
|
| 34 | +import org.springframework.util.FileSystemUtils; |
| 35 | + |
34 | 36 | import static org.assertj.core.api.Assertions.assertThat;
|
35 | 37 | import static org.assertj.core.api.Assertions.assertThatCode;
|
36 | 38 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
|
40 | 42 | * Tests for {@link FileWatcher}.
|
41 | 43 | *
|
42 | 44 | * @author Moritz Halbritter
|
| 45 | + * @author Brian Clozel |
43 | 46 | */
|
44 | 47 | class FileWatcherTests {
|
45 | 48 |
|
@@ -191,6 +194,74 @@ void testRelativeDirectories() throws Exception {
|
191 | 194 | }
|
192 | 195 | }
|
193 | 196 |
|
| 197 | + /* |
| 198 | + * Replicating a letsencrypt folder structure like: |
| 199 | + * "/folder/live/certname/privkey.pem -> ../../archive/certname/privkey32.pem" |
| 200 | + */ |
| 201 | + @Test |
| 202 | + void shouldFollowRelativePathSymlinks(@TempDir Path tempDir) throws Exception { |
| 203 | + Path folder = tempDir.resolve("folder"); |
| 204 | + Path live = folder.resolve("live").resolve("certname"); |
| 205 | + Path archive = folder.resolve("archive").resolve("certname"); |
| 206 | + Path link = live.resolve("privkey.pem"); |
| 207 | + Path targetFile = archive.resolve("privkey32.pem"); |
| 208 | + Files.createDirectories(live); |
| 209 | + Files.createDirectories(archive); |
| 210 | + Files.createFile(targetFile); |
| 211 | + Path relativePath = Path.of("../../archive/certname/privkey32.pem"); |
| 212 | + Files.createSymbolicLink(link, relativePath); |
| 213 | + try { |
| 214 | + WaitingCallback callback = new WaitingCallback(); |
| 215 | + this.fileWatcher.watch(Set.of(link), callback); |
| 216 | + Files.writeString(targetFile, "Some content"); |
| 217 | + callback.expectChanges(); |
| 218 | + } |
| 219 | + finally { |
| 220 | + FileSystemUtils.deleteRecursively(folder); |
| 221 | + } |
| 222 | + } |
| 223 | + |
| 224 | + /* |
| 225 | + * Replicating a k8s configmap folder structure like: |
| 226 | + * "secret.txt -> ..data/secret.txt", |
| 227 | + * "..data/ -> ..a72e81ff-f0e1-41d8-a19b-068d3d1d4e2f/", |
| 228 | + * "..a72e81ff-f0e1-41d8-a19b-068d3d1d4e2f/secret.txt" |
| 229 | + * |
| 230 | + * After a secret update, this will look like: "secret.txt -> ..data/secret.txt", |
| 231 | + * "..data/ -> ..bba2a61f-ce04-4c35-93aa-e455110d4487/", |
| 232 | + * "..bba2a61f-ce04-4c35-93aa-e455110d4487/secret.txt" |
| 233 | + */ |
| 234 | + @Test |
| 235 | + void shouldTriggerOnConfigMapUpdates(@TempDir Path tempDir) throws Exception { |
| 236 | + Path configMap1 = createConfigMap(tempDir, "secret.txt"); |
| 237 | + Path configMap2 = createConfigMap(tempDir, "secret.txt"); |
| 238 | + Path data = tempDir.resolve("..data"); |
| 239 | + Files.createSymbolicLink(data, configMap1); |
| 240 | + Path secretFile = tempDir.resolve("secret.txt"); |
| 241 | + Files.createSymbolicLink(secretFile, data.resolve("secret.txt")); |
| 242 | + try { |
| 243 | + WaitingCallback callback = new WaitingCallback(); |
| 244 | + this.fileWatcher.watch(Set.of(secretFile), callback); |
| 245 | + Files.delete(data); |
| 246 | + Files.createSymbolicLink(data, configMap2); |
| 247 | + FileSystemUtils.deleteRecursively(configMap1); |
| 248 | + callback.expectChanges(); |
| 249 | + } |
| 250 | + finally { |
| 251 | + FileSystemUtils.deleteRecursively(configMap2); |
| 252 | + Files.delete(data); |
| 253 | + Files.delete(secretFile); |
| 254 | + } |
| 255 | + } |
| 256 | + |
| 257 | + Path createConfigMap(Path parentDir, String secretFileName) throws IOException { |
| 258 | + Path configMapFolder = parentDir.resolve(".." + UUID.randomUUID()); |
| 259 | + Files.createDirectory(configMapFolder); |
| 260 | + Path secret = configMapFolder.resolve(secretFileName); |
| 261 | + Files.createFile(secret); |
| 262 | + return configMapFolder; |
| 263 | + } |
| 264 | + |
194 | 265 | private static final class WaitingCallback implements Runnable {
|
195 | 266 |
|
196 | 267 | private final CountDownLatch latch = new CountDownLatch(1);
|
|
0 commit comments