Fabric8 Kubernetes and Openshift Java DSL
Intro
The first releases of the Fabric8 v2 have been using a JAX-RS based Kubernetes client that was using ApacheCXF. The client was great, but we always wanted to provide something thinner, with less dependencies (so that its easier to adopt). We also wanted to give it a fecelift and build a DSL around it so that it becomes easier to use and read.
The new client currently lives at: https://github.com/fabric8io/kubernetes-client and it provides the following modules:
- A Kubernetes client.
- An Openshift client.
- Mocking framework for all the above (based on EasyMock)
A first glance at the client
Let’s have a quick look on how you can create, list and delete things using the client:
//Instantiate the client KubernetesClient client = new DefaultKubernetesClient(); //Create a service Service myservice = ...; client.services().inNamespace("fabric8").create(myservice); //Create a service inline Service jenkins = client.services().inNamespace("fabric8").createNew() .withNewMetadata() .withName("jenkins") .addToLabels("component", "jenkins") .endMetadata() .done(); //List services ServiceList serviceList = client.services().inNamespace("fabric8").list(); //Watch services client.services().inNamespace("fabric8").watch(new Watcher<Service>() { @Override public void eventReceived(Action action, Service resource) { logger.info("{}: {}", action, resource); } }); //Delete by label Boolean deleted = client.services().withLabel("component", "jenkins").delete(); //Close client client.close();
The snippet above is pretty much self explanatory (and that’s the beauty of using a DSL), but I still have a blog post to fill, so I’ll provide as many details as possible.
The client domain model
You could think of the client as a union of two things:
- The Kubernetes domain model.
- The DSL around the model.
The domain model is a set of objects that represents the data that are exchanged between the client and Kubernetes /Openshift. The raw format of the data is JSON. These JSON objects are quite complex and their structure is pretty strict, so hand crafting them is not a trivial task.
We needed to have a way of manipulating these JSON objects in Java (and being able to take advantage of code completion etc) but also stay as close as possible to the original format. Using a POJO representation of the JSON objects can be used for manipulation, but it doesn’t quite feel like JSON and is also not really usable for JSON with deep nesting. So instead, we decided to generate fluent builders on top of those POJOs that used the exact same structure with the original JSON.
For example, here the JSON object of a Kubernetes Service:
{ "kind": "Service", "metadata": { "name": "kubernetes", "namespace": "default", "labels": { "component": "apiserver", "provider": "kubernetes" } }, "spec": { "ports": [ { "name": "", "protocol": "TCP", "port": 443, "targetPort": 443 } ], "selector": null, "portalIP": "172.30.17.2", "sessionAffinity": "None" }, "status": {} }
The Java equivalent using Fluent Builders could be:
Service srv = new ServiceBuilder() .withNewMetadata() .withName("kubernetes") .addToLabels("component", "apiserver") .addToLabels("provider", "kubernetes") .endMetadata() .withNewSpec() .addNewPort() .withProtocol("TCP") .withPort(443) .withNewTargetPort(443) .endPort() .withPortalIP("172.30.17.2") .withSessionAffinity("None") .endSpec() .build();
The domain model lives on its own project: Fabric8’s Kubernetes Model. The model is generated from Kubernetes and Openshift code after a long process:
- Go source conversion JSON schema
- JSON schema conversion POJO
- Generation of Fluent Builders
Fluent builders are generated by a tiny project called sundrio, which I’ll cover in a future post.
Getting an instance of the client
Getting an instance of the default client instance is pretty trivial since an empty constructor is provided. When the empty constructor is used the client will use the default settings which are:
- Kubernetes URL
- System property “kubernetes.master“
- Environment variable “KUBERNETES_MASTER“
- From “.kube/config” file inside user home.
- Using DNS: “https://kubernetes.default.svc“
- Service account path “/var/run/secrets/kubernetes.io/serviceaccount/“
More fine grained configuration can be provided by passing an instance of the Config object.
//Client with custom config Config config = new ConfigBuilder() .withMasterUrl(url) .withTrustCerts(true) .withOauthToken(mytoken) .build(); KubernetesClient = new DefaultKubernetesClient(config);
Client extensions and adapters
To support Kubernetes extensions (e.g Openshift) the client uses the notion of the Extension and the Adapter. The idea is pretty simple. An extension client extends the default client and implements the Extension. Each client instance can be adapted to the Extension as long as an Adapter can be found via Java’s ServiceLoader(forgive me father).
Here’s an example of how to adapt any instance of the client to an instance of the OpenshiftClient:
KubernetesClient client = new DefaultKubernetesClinet(); OpenShiftClient oc = client.adapt(OpenShiftClient.class);
The code above will work only if /oapi exists in the list of root paths returned by the Kubernetes Client (i.e. the client points to an open shift installation). If not it will throw an IllegalArugementException.
In case the user is writing code that is bound to Openshift he can always directly instantiate an Instance of the default openshift client.
//Openshift client with custom config OpenshiftConfig config = new OpenshiftConfigBuilder() .withMasterUrl(url) .withOpenShiftUrl(openshiftUrl) .withTrustCerts(true) .build(); OpenshiftClient client = new DefaultOpenshiftClient(config);
Testing and Mocking
Mocking a client that is talking to an external system is a pretty common case. When the client is flat
(doesn’t support method chaining) mocking is trivial and there are tons of frameworks out there that can be used for the job. When using a DSL though, things get more complex and require a lot of boilerplate code to wire the pieces together. If the reason is not obvious, let’s just say that with mocks you define the behaviour of the mock per method invocation. DSLs tend to have way more methods (with fewer arguments) compared to the equivalent Flat objects. That alone increases the work needed to define the behaviour. Moreover, those methods are chained together by returning intermediate objects, which means that they need to be mocked too, which further increases both the workload and the complexity.
To remove all the boilerplate and make mocking the client pretty trivial to use we combined the DSL of the client, with the DSL of a mocking framework: EasyMock. This means that the entry point to this DSL is the Kubernetes client DSL itself, but the terminal methods have been modified so that they return “Expectation Setters”. An example should make this easier to comprehend.
KubernetesMockClient mock = new KubernetesMockClient(); //Define the behaviour mock.services().inNamespace(or("default","fabric8")).withName("fabric8-console-service").get().andReturn( new ServiceBuilder() .withNewMetadata().withName("fabric8-console-service").endMetadata() .withNewSpec() .addNewPort() .withProtocol("TCP") .withPort(80) .withNewTargetPort(9090) .endPort() .endSpec() .build() ).anyTimes(); //Get an instance of the client mock KubernetesClient client = mock.replay(); //Use the client Assert.assertNotNull(client.services().inNamespace("fabric8").withName("fabric8-console-service").get()); Assert.assertNotNull(client.services().inNamespace("default").withName("fabric8-console-service").get()); //Verify the client EasyMock.verify(client);
The mocking framework can be easily combined with other Fabric8 components, like the CDI extension. You just have to create @Produces method that returns the mock.
Enjoy!
Reference: | Fabric8 Kubernetes and Openshift Java DSL from our JCG partner Ioannis Canellos at the Ioannis Canellos Blog blog. |