Skip to content

Commit 6a94d7f

Browse files
authored
Make SslTest that verifies keystore reloading not flakey (#208)
- 1 don't have a single static thread pool that all FileWatchers use - 2 update tests to cleanup temp/test keystore files Also, introduced awaitility test library
1 parent bd14613 commit 6a94d7f

File tree

6 files changed

+112
-41
lines changed

6 files changed

+112
-41
lines changed

core/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,5 +142,10 @@
142142
<version>${guava.version}</version>
143143
<scope>test</scope>
144144
</dependency>
145+
<dependency>
146+
<groupId>org.awaitility</groupId>
147+
<artifactId>awaitility</artifactId>
148+
<scope>test</scope>
149+
</dependency>
145150
</dependencies>
146151
</project>

core/src/main/java/io/confluent/rest/ApplicationServer.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public final class ApplicationServer<T extends RestConfig> extends Server {
5454
private final T config;
5555
private final ApplicationGroup applications;
5656
private final SslContextFactory sslContextFactory;
57+
private FileWatcher sslKeystoreFileWatcher;
5758

5859
private List<NetworkTrafficServerConnector> connectors = new ArrayList<>();
5960

@@ -177,6 +178,9 @@ private void finalizeHandlerCollection(HandlerCollection handlers, HandlerCollec
177178
protected void doStop() throws Exception {
178179
super.doStop();
179180
applications.doStop();
181+
if (sslKeystoreFileWatcher != null) {
182+
sslKeystoreFileWatcher.shutdown();
183+
}
180184
}
181185

182186
protected final void doStart() throws Exception {
@@ -269,7 +273,9 @@ private SslContextFactory createSslContextFactory(RestConfig config) {
269273
if (config.getBoolean(RestConfig.SSL_KEYSTORE_RELOAD_CONFIG)) {
270274
Path watchLocation = getWatchLocation(config);
271275
try {
272-
FileWatcher.onFileChange(watchLocation, () -> {
276+
// create and shutdown a sslKeystoreFileWatcher for each Application, so that
277+
// all Applications in the same JVM don't use the same shared threadpool
278+
sslKeystoreFileWatcher = FileWatcher.onFileChange(watchLocation, () -> {
273279
// Need to reset the key store path for symbolic link case
274280
sslContextFactory.setKeyStorePath(
275281
config.getString(RestConfig.SSL_KEYSTORE_LOCATION_CONFIG)

core/src/main/java/io/confluent/rest/FileWatcher.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
// reference https://gist.github.com/danielflower/f54c2fe42d32356301c68860a4ab21ed
3636
public class FileWatcher implements Runnable {
3737
private static final Logger log = LoggerFactory.getLogger(FileWatcher.class);
38-
private static final ExecutorService executor = Executors.newFixedThreadPool(1,
38+
39+
// don't have static shared threadpool for all FileWatchers in the JVM
40+
private final ExecutorService executor = Executors.newFixedThreadPool(1,
3941
new ThreadFactory() {
4042
public Thread newThread(Runnable r) {
4143
Thread t = Executors.defaultThreadFactory().newThread(r);
@@ -67,10 +69,11 @@ public FileWatcher(Path file, Callback callback) throws IOException {
6769
* Starts watching a file calls the callback when it is changed.
6870
* A shutdown hook is registered to stop watching.
6971
*/
70-
public static void onFileChange(Path file, Callback callback) throws IOException {
72+
public static FileWatcher onFileChange(Path file, Callback callback) throws IOException {
7173
log.info("Configure watch file change: " + file);
7274
FileWatcher fileWatcher = new FileWatcher(file, callback);
73-
executor.submit(fileWatcher);
75+
fileWatcher.executor.submit(fileWatcher);
76+
return fileWatcher;
7477
}
7578

7679
public void run() {

core/src/test/java/io/confluent/rest/ApiHeadersTest.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.junit.AfterClass;
4646
import org.junit.BeforeClass;
4747
import org.junit.Test;
48+
import org.junit.rules.TemporaryFolder;
4849

4950
public class ApiHeadersTest {
5051

@@ -56,11 +57,16 @@ public class ApiHeadersTest {
5657
private static String clientKeystoreLocation;
5758
private static TestApplication app;
5859

60+
// Use a temporary folder so that .jks files created by this test are isolated
61+
// and deleted when the test is done
62+
public static TemporaryFolder tempFolder = new TemporaryFolder();
63+
5964
@BeforeClass
6065
public static void setUp() throws Exception {
61-
final File trustStore = File.createTempFile("ApiHeadersTest-truststore", ".jks");
62-
final File clientKeystore = File.createTempFile("ApiHeadersTest-client-keystore", ".jks");
63-
final File serverKeystore = File.createTempFile("ApiHeadersTest-server-keystore", ".jks");
66+
tempFolder.create();
67+
final File trustStore = File.createTempFile("ApiHeadersTest-truststore", ".jks", tempFolder.getRoot());
68+
final File clientKeystore = File.createTempFile("ApiHeadersTest-client-keystore", ".jks", tempFolder.getRoot());
69+
final File serverKeystore = File.createTempFile("ApiHeadersTest-server-keystore", ".jks", tempFolder.getRoot());
6470

6571
clientKeystoreLocation = clientKeystore.getAbsolutePath();
6672

@@ -84,6 +90,7 @@ public static void teardown() throws Exception {
8490
if (app != null) {
8591
app.stop();
8692
}
93+
tempFolder.delete();
8794
}
8895

8996
@Test

core/src/test/java/io/confluent/rest/SslTest.java

Lines changed: 77 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.fasterxml.jackson.annotation.JsonProperty;
2020

21+
import java.util.concurrent.TimeUnit;
2122
import org.apache.http.client.ClientProtocolException;
2223
import org.apache.http.client.methods.CloseableHttpResponse;
2324
import org.apache.http.client.methods.HttpGet;
@@ -30,8 +31,10 @@
3031
import org.apache.kafka.common.config.types.Password;
3132
import org.apache.kafka.test.TestSslUtils;
3233
import org.apache.kafka.test.TestSslUtils.CertificateBuilder;
33-
import org.junit.Before;
34+
import org.junit.AfterClass;
35+
import org.junit.BeforeClass;
3436
import org.junit.Test;
37+
import org.junit.rules.TemporaryFolder;
3538
import org.slf4j.Logger;
3639
import org.slf4j.LoggerFactory;
3740

@@ -60,28 +63,49 @@
6063
import static org.junit.Assert.assertEquals;
6164
import static org.junit.Assert.assertNotEquals;
6265
import static org.junit.Assert.assertTrue;
66+
import static org.awaitility.Awaitility.await;
67+
import static org.junit.Assert.fail;
6368

6469
public class SslTest {
6570
private static final Logger log = LoggerFactory.getLogger(SslTest.class);
6671

67-
private File trustStore;
68-
private File clientKeystore;
69-
private File serverKeystore;
70-
private File serverKeystoreBak;
71-
private File serverKeystoreErr;
72+
private static File trustStore;
73+
private static File clientKeystore;
74+
private static File serverKeystore;
75+
private static File serverKeystoreBak;
76+
private static File serverKeystoreErr;
7277

7378
public static final String SSL_PASSWORD = "test1234";
7479
public static final String EXPECTED_200_MSG = "Response status must be 200.";
75-
public static final int CERT_RELOAD_WAIT_TIME = 30000;
76-
77-
@Before
78-
public void setUp() throws Exception {
80+
public static final int CERT_RELOAD_WAIT_TIME = 20000;
81+
82+
private static TemporaryFolder tempFolder;
83+
84+
@BeforeClass
85+
public static void setUp() throws Exception {
86+
87+
/*
88+
* To make this test less flakey
89+
* - 1 don't create keystore files for every test method
90+
* - 2 cleanup keystore files on test exist so they don't have to be considerd by the FileWatcher
91+
* - 3 updated the FileWatcher and Application class to not have a single shared threadpool to
92+
* watch for changed files.
93+
*
94+
* By default temp files are not cleaned when up. Which isn't normally a problem unless you are
95+
* testing the ability of rest-utils apps to notice and reload updated ssl keystore files.
96+
*
97+
* Turns out the temp dir that Java+MacOs was continually using on my local machine had 1500
98+
* files in it. Also the Java "FileWatcher" for Mac works via polling a directory,
99+
* this seems to have added to the flakeyness.
100+
*/
101+
tempFolder = new TemporaryFolder();
102+
tempFolder.create();
79103
try {
80-
trustStore = File.createTempFile("SslTest-truststore", ".jks");
81-
clientKeystore = File.createTempFile("SslTest-client-keystore", ".jks");
82-
serverKeystore = File.createTempFile("SslTest-server-keystore", ".jks");
83-
serverKeystoreBak = File.createTempFile("SslTest-server-keystore", ".jks.bak");
84-
serverKeystoreErr = File.createTempFile("SslTest-server-keystore", ".jks.err");
104+
trustStore = File.createTempFile("SslTest-truststore", ".jks", tempFolder.getRoot());
105+
clientKeystore = File.createTempFile("SslTest-client-keystore", ".jks", tempFolder.getRoot());
106+
serverKeystore = File.createTempFile("SslTest-server-keystore", ".jks", tempFolder.getRoot());
107+
serverKeystoreBak = File.createTempFile("SslTest-server-keystore", ".jks.bak", tempFolder.getRoot());
108+
serverKeystoreErr = File.createTempFile("SslTest-server-keystore", ".jks.err", tempFolder.getRoot());
85109
} catch (IOException ioe) {
86110
throw new RuntimeException("Unable to create temporary files for trust stores and keystores.");
87111
}
@@ -95,7 +119,12 @@ public void setUp() throws Exception {
95119
createWrongKeystoreWithCert(serverKeystoreErr, "server", certs);
96120
}
97121

98-
private void createKeystoreWithCert(File file, String alias, Map<String, X509Certificate> certs) throws Exception {
122+
@AfterClass
123+
public static void teardown() {
124+
tempFolder.delete();
125+
}
126+
127+
private static void createKeystoreWithCert(File file, String alias, Map<String, X509Certificate> certs) throws Exception {
99128
KeyPair keypair = TestSslUtils.generateKeyPair("RSA");
100129
CertificateBuilder certificateBuilder = new CertificateBuilder(30, "SHA1withRSA");
101130
X509Certificate cCert = certificateBuilder.sanDnsName("localhost")
@@ -128,7 +157,7 @@ private void enableSslClientAuth(Properties props) {
128157
props.put(RestConfig.SSL_CLIENT_AUTH_CONFIG, true);
129158
}
130159

131-
private void createWrongKeystoreWithCert(File file, String alias, Map<String, X509Certificate> certs) throws Exception {
160+
private static void createWrongKeystoreWithCert(File file, String alias, Map<String, X509Certificate> certs) throws Exception {
132161
KeyPair keypair = TestSslUtils.generateKeyPair("RSA");
133162
CertificateBuilder certificateBuilder = new CertificateBuilder(30, "SHA1withRSA");
134163
X509Certificate cCert = certificateBuilder.sanDnsName("fail")
@@ -176,30 +205,44 @@ public void testHttpsWithAutoReload() throws Exception {
176205
SslTestApplication app = new SslTestApplication(config);
177206
try {
178207
app.start();
179-
int statusCode = makeGetRequest(httpsUri + "/test",
208+
int startingCode = makeGetRequest(httpsUri + "/test",
180209
clientKeystore.getAbsolutePath(), SSL_PASSWORD, SSL_PASSWORD);
181-
assertEquals(EXPECTED_200_MSG, 200, statusCode);
210+
assertEquals(EXPECTED_200_MSG, 200, startingCode);
182211
assertMetricsCollected();
183212

184213
// verify reload -- override the server keystore with a wrong one
185214
Files.copy(serverKeystoreErr.toPath(), serverKeystore.toPath(), StandardCopyOption.REPLACE_EXISTING);
186-
Thread.sleep(CERT_RELOAD_WAIT_TIME);
187-
boolean hitError = false;
188-
try {
189-
makeGetRequest(httpsUri + "/test",
190-
clientKeystore.getAbsolutePath(), SSL_PASSWORD, SSL_PASSWORD);
191-
} catch (Exception e) {
192-
System.out.println(e);
193-
hitError = true;
194-
}
215+
log.info("\tKeystore reload test : Applied bad keystore file");
216+
217+
await().pollInterval(2, TimeUnit.SECONDS).atMost(30, TimeUnit.SECONDS).untilAsserted( () -> {
218+
boolean hitError = false;
219+
try {
220+
log.info("\tKeystore reload test : Awaiting failed https connection");
221+
makeGetRequest(httpsUri + "/test", clientKeystore.getAbsolutePath(), SSL_PASSWORD, SSL_PASSWORD);
222+
} catch (Exception e) {
223+
System.out.println(e);
224+
hitError = true;
225+
}
226+
assertTrue("Expecting to hit an error with new server cert", hitError);
227+
});
195228

196229
// verify reload -- override the server keystore with a correct one
197230
Files.copy(serverKeystoreBak.toPath(), serverKeystore.toPath(), StandardCopyOption.REPLACE_EXISTING);
198-
Thread.sleep(CERT_RELOAD_WAIT_TIME);
199-
statusCode = makeGetRequest(httpsUri + "/test",
200-
clientKeystore.getAbsolutePath(), SSL_PASSWORD, SSL_PASSWORD);
201-
assertEquals(EXPECTED_200_MSG, 200, statusCode);
202-
assertEquals("expect hit error with new server cert", true, hitError);
231+
log.info("\tKeystore reload test : keystore set back to good value");
232+
233+
await().pollInterval(2, TimeUnit.SECONDS).atMost(30, TimeUnit.SECONDS).untilAsserted( () -> {
234+
try {
235+
log.info("\tKeystore reload test : Awaiting a valid https connection");
236+
int statusCode = makeGetRequest(httpsUri + "/test", clientKeystore.getAbsolutePath(), SSL_PASSWORD, SSL_PASSWORD);
237+
assertEquals(EXPECTED_200_MSG, 200, statusCode);
238+
log.info("\tKeystore reload test : Valid connection found");
239+
}
240+
catch (Exception e) {
241+
fail();
242+
// we have to wait for the good key to take affect
243+
}
244+
});
245+
203246
} finally {
204247
if (app != null) {
205248
app.stop();
@@ -332,7 +375,7 @@ public void testHttpsWithAuthAndBadClientCert() throws Exception {
332375
app.start();
333376

334377
// create a new client cert that isn't in the server's trust store.
335-
File untrustedClient = File.createTempFile("SslTest-client-keystore", ".jks");
378+
File untrustedClient = File.createTempFile("SslTest-client-keystore", ".jks", tempFolder.getRoot());
336379
Map<String, X509Certificate> certs = new HashMap<>();
337380
createKeystoreWithCert(untrustedClient, "client", certs);
338381
try {

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
<asynchttpclient.version>2.2.0</asynchttpclient.version>
5656
<guava.version>24.0-jre</guava.version>
5757
<checkstyle.suppressions.location>checkstyle/suppressions.xml</checkstyle.suppressions.location>
58+
<awaitility.version>4.0.3</awaitility.version>
5859
</properties>
5960

6061
<repositories>
@@ -182,6 +183,12 @@
182183
<artifactId>jersey-test-framework-provider-jetty</artifactId>
183184
<version>${jersey.version}</version>
184185
</dependency>
186+
187+
<dependency>
188+
<groupId>org.awaitility</groupId>
189+
<artifactId>awaitility</artifactId>
190+
<version>${awaitility.version}</version>
191+
</dependency>
185192
</dependencies>
186193
</dependencyManagement>
187194

0 commit comments

Comments
 (0)