The first releases of the
Fabric8 v2 have been using a JAX-RS based
Kubernetes client that was using
Apache CXF. 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 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:
The Java equivalent using Fluent Builders could be:
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 extensions and adapters
Here's an example of how to adapt any instance of the client to an instance of the
OpenshiftClient:
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.
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.
The mocking framework can be easily combined with other