DevOps

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:
 
 

  1. Kubernetes client.
  2. An Openshift client.
  3. 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:

  1. The Kubernetes domain model.
  2. 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:

  1. Go source conversion JSON schema
  2. JSON schema conversion POJO
  3. 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
    1. System property “kubernetes.master
    2. Environment variable “KUBERNETES_MASTER
    3. From “.kube/config” file inside user home.
    4. 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.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button