Testing an HTTPS client for the Kubernetes API server using Hoverfly Java

Tommy Situ,

Hoverfly is a lightweight API simulation tool which can be used to simulate HTTP/HTTPS traffic. In this article, I’ll be demonstrating how it can be used to help test my HTTPS client.  Thanks to Hoverfly Java’s support for JUnit - and it’s DSL -  creating an HTTPS server for testing is a breeze compared to the existing Java solutions. I also get the added bonus of a powerful API simulation tool which can be used to simulate the target server endpoint. 

The problem with developing an HTTPS client

When working on the upcoming launch of our cloud service at SpectoLabs (stay tuned!), I came across an interesting problem.

One of the services running in our Kubernetes cluster needed to access the Kubernetes apiserver to manage Hoverfly deployment via an HTTPS connection. As a requirement, I needed to verify the authenticity of the apiserver using the SSL certificate /var/run/secrets/kubernetes.io/serviceaccount/ca.crt located in the pod filesystem. 

Basically, I needed to configure an HTTPS client to use an externally provided certificate. It is rather verbose to accomplish this in Java. A lot of online tutorials are devoted to this topic and the example code is usually boiled down to the following steps:

  1. Create a truststore from the PEM-encoded cert file
  2. Initialize a trust factory from the truststore and create a trust manager
  3. Initialize an SSL context given the trust manager
  4. Set up an HTTPS client to use the SSL context

It is hard to resist the temptation to copy and paste code, then sit back and relax. But blindly adding untested code to a project is a horrible idea - the most common expected failure for an HTTPS client being an SSLHandshakeException:

org.springframework.web.client.ResourceAccessException: I/O error on GET request for "https://kubernetes/api/v1/namespaces/default/services/hoverfly": sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target; nested exception is javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

Without proper testing, this kind of error is hard to debug. A fix cannot be verified until the application is deployed to a real environment. It is also not guaranteed that your one-time fix won’t break again in the future.

Going TDD

I’ve decided to go TDD, but where do I start? My gut feeling is that this test is not going to be pretty. I would have to create an HTTPS server using jetty or similar, run a bunch of openssl and keytool commands to generate SSL certificate and key, and in turn create the truststore and keystore to be used by the test server.

Not to mention that I also need to configure a server handler to handle requests from my test. But all I want to test is that my HTTPS client is configured correctly, and that the SSL handshake can be established.

Apparently creating such a test is a very cumbersome process and is difficult to justify the amount of development time spent on it.

Hoverfly to the rescue

First, I simply used the Hoverfly command line tool (hoverctl) to generate a self-signed certificate and key pair (more information on how to install Hoverfly can be found here):

hoverfly --generate-ca-cert

The command creates a cert.pem and key.pem file in the directory where it is run. I copied and pasted them into the test resources folder,  where they would be ready for use.

In the JUnit test, all I did was add the HoverflyRule, and pass the SSL cert and key path to the configs object in order to start up an HTTPS server.

@ClassRule
public static HoverflyRule hoverflyRule = HoverflyRule.inSimulationMode(
        configs()
                .sslCertificatePath("ssl/ca.crt")
                .sslKeyPath("ssl/ca.key"));

Then, I wrote the test for my Kubernetes gateway. Using the Hoverfly Java DSL, I was also able to simulate the Kubernetes GET service endpoint, telling it to return a 200 response.

@Test
public void shouldBeAbleToCallHttpsServerUsingSelfSignedCertificate() throws Exception {

    // Given
    K8sService expected = new K8sService(new Metadata("hoverfly", "some-uuid", "default"), new Spec());
    hoverflyRule.simulate(dsl(
            service("https://kubernetes")
                    .get("/api/v1/namespaces/default/services/hoverfly")
                    .willReturn(success(HttpBodyConverter.json(expected)))
    ));

    // When
    K8sService actual = k8sGateway.getService("hoverfly");

    // Then
    assertThat(actual).isEqualTo(expected);
}

Thanks to the above test, I am now getting fast feedback on my HTTPS client configuration. In this example, I configured a RestTemplate from the Spring MVC project (although I could have used any HTTP client library) which was exposed as a bean and autowired into the Kubernetes gateway.


@Bean
public RestTemplate k8sRestTemplate(K8sConfig k8sConfig) {

	RestTemplate restTemplate = new RestTemplate();

	Path caCertFile = Paths.get(k8sConfig.getCaCertPath());

	if (Files.exists(caCertFile) && Files.isRegularFile(caCertFile)) {
		SSLContext sslContext = SslUtils.createSslContextFromCertFile(caCertFile);
		CloseableHttpClient httpsClient = HttpClients.custom().useSystemProperties().setSSLContext(sslContext).build();
		restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(httpsClient));

	} else {
		LOGGER.warn("Certificate file {} is not found, Kubernetes api client will not be configured to use HTTPS protocol.", caCertFile.toString());
		restTemplate.setRequestFactory(new SimpleClientHttpRequestFactory());
	}

	return restTemplate;
}

Conclusion

Besides being a useful an API simulation tool, Hoverfly Java provides a straightforward and elegant solution to the HTTPS testing problem. For the production system, an HTTPS client may be configured with other features, such as authentication or logging interceptors, which would only make testing more difficult. An integration test for the happy path would definitely help to shorten the debugging/development cycle.

If you are interested in the source code for this blog, it can be found on GitHub.