Write a kubectl plugin in Java with jbang and fabric8
Tags: kubernetes, java
Published: 2020-12-26 12:48:09

If you use `kubectl` frequently, you might be find yourself running the same sequence of commands over and over again.

A frequent use case yielding such sequences is for `viewing a secret` which requires to find the secret and decode its base64 form.

In this tutorial, I'll go through how to create a `kubectl` plugin in Java. The plugin will do the following after we execute the `kubectl lp` command : 

- List the pods with their name, status and a default empty message
- If the pod is not in running state, display a :fire: icon in front of its line, otherwise display a :white_check_mark: icon
- Display a message why the pod is not running

---

## Jbang

To write the plugin in Java, I will use [jbang](https://github.com/jbangdev/jbang). 
Jbang is a framework that brings scripting to the Java land.

If you don't have Jbang yet installed, just run the following : 

```bash
brew install jbangdev/tap/jbang
```

Or check the [documentation](https://github.com/jbangdev/jbang#installation) to find the instructions for your operating system.

The next step is to create a template with the initializer, I'll call the script `lp` for `list pods` : 

```bash
jbang init --template=cli lp.java
```

It will create a fully functional script `lp.java` with the following content : 

```java
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS info.picocli:picocli:4.5.0

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;

import java.util.concurrent.Callable;

@Command(name = "lp", mixinStandardHelpOptions = true, version = "lp 0.1",
        description = "lp made with jbang")
class lp implements Callable<Integer> {

    @Parameters(index = "0", description = "The greeting to print", defaultValue = "World!")
    private String greeting;

    public static void main(String... args) {
        int exitCode = new CommandLine(new lp()).execute(args);
        System.exit(exitCode);
    }

    @Override
    public Integer call() throws Exception { // your business logic goes here...
        System.out.println("Hello " + greeting);
        return 0;
    }
}

```

Which is a simple template that greets you, try it with the following command : 

```bash
$ jbang lp.java Chris
Hello Chris
$
```

Adding dependencies to your scripts is as simple as adding a comment in the beginning of the file with the dependency artifact : 

```java
//DEPS io.fabric8:kubernetes-client:4.13.0
```

Here, we added the `fabric8` kubernetes client library. While we're at it, let's see how this client works.

---

## Fabric8

The [fabric8](https://github.com/fabric8io/kubernetes-client) kubernetes client is a library that provides access to the Kubernetes Rest API via a simple and human friendly DSL.

For eg, here is how you find only the pods that are not in a `RUNNING` status : 

```java
kc.pods().list().getItems()
                .stream()
                .filter(pod -> !PodStatusUtil.isRunning(pod))
                .collect(Collectors.toList());
```

For our scenario, we will use the client to retrieve all the pods and create a new domain model `PodInfo` with the name, the state and an optional message : 

```java
private static List<PodInfo> getPods() {
        KubernetesClient kc;
        try {
            kc = new DefaultKubernetesClient();
        } catch (Exception e) {
            throw new RuntimeException("Unable to create default Kubernetes client", e);
        }

        return kc.pods().list().getItems().stream().map(pod -> {
            PodInfoState state = PodStatusUtil.isRunning(pod) ? PodInfoState.RUNNING : PodInfoState.FAILING;
            String message = null;
            if (!state.equals(PodInfoState.RUNNING)) {
                message = PodStatusUtil.getContainerStatus(pod).get(0).getState().getWaiting().getMessage();
            }

            return new PodInfo(pod.getMetadata().getName(), state, message);
        }).collect(Collectors.toList());
    }

    static class PodInfo {

        private final String name;
        private final PodInfoState state;
        private final String message;

        public PodInfo(String name, PodInfoState state, String message) {
            this.name = name;
            this.state = state;
            this.message = message;
        }

        public String getName() {
            return name;
        }

        public PodInfoState getState() {
            return state;
        }

        public String getMessage() {
            return message;
        }
    }

    enum PodInfoState {
        RUNNING,
        FAILING
    }
```

We'll also add the `j-text-utils` java library so we can display everything in a table 

```
//DEPS com.massisframework:j-text-utils:0.3.4
```

```java
private static void printTable(List<PodInfo> list) {
        final Object[][] tableData = list.stream()
                .map(podInfo -> new Object[]{
                        podInfo.getState().equals(PodInfoState.RUNNING) ? CHECK_MARK : FIRE,
                        podInfo.getName(),
                        podInfo.getState(),
                        podInfo.getMessage()
                })
                .toArray(Object[][]::new);
        String[] columnNames = {"", "name", "state", "message"};
        new TextTable(columnNames, tableData).printTable();
    }
```

The full code can be found here : https://github.com/ikwattro/kubectl-plugin-java-jbang


If we run our script, we can see it in action : 

```bash
$ jbang lp.java
[jbang] Building jar...
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
________________________________________________________________________________________
|   | name                             | state  | message                               |
|=======================================================================================|
| ✅ | neo4j-neo4j-core-0               | RUNNING|                                       |
| ✅ | nginx-app                        | RUNNING|                                       |
| 🔥| nginx-deployment-77fbdb7874-8gkcr| FAILING| Back-off pulling image "nginx2:1.16.1"|
| 🔥| nginx-deployment-77fbdb7874-8jps7| FAILING| Back-off pulling image "nginx2:1.16.1"|
| 🔥| nginx-deployment-77fbdb7874-vx27r| FAILING| Back-off pulling image "nginx2:1.16.1"|
| ✅ | postgres                         | RUNNING|                                       |
| ✅ | coredns-f9fd979d6-qwgfm          | RUNNING|                                       |
| ✅ | etcd-minikube                    | RUNNING|                                       |
| ✅ | kube-apiserver-minikube          | RUNNING|                                       |
| ✅ | kube-controller-manager-minikube | RUNNING|                                       |
| ✅ | kube-proxy-s5bcc                 | RUNNING|                                       |
| ✅ | kube-scheduler-minikube          | RUNNING|                                       |
| ✅ | storage-provisioner              | RUNNING|                                       |
$
```

---

## Extending kubectl

Extending `kubectl` is very easy as it has mainly 3 conventions : 

- You need to place your script in your path
- The name of the script should start with `kubectl-` and not have an extension
- The script should be executable

We will need to pay attention, because when generating the `jbang` template, the very first line was commented for playing nicely with IDEs when editing the script.

Just uncomment the line : 

```java
#!/usr/bin/env jbang
``` 

Secondly, we will copy the script to our path : 

```bash
cp lp.java /usr/local/bin/kubectl-lp
```

And make it executable : 

```bash
chmod +x /usr/local/bin/kubectl-lp
```

You can now run `kubectl lp` for listing your pods : 

```bash
$ kubectl lp
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
________________________________________________________________________________________
|   | name                             | state  | message                               |
|=======================================================================================|
| ✅ | neo4j-neo4j-core-0               | RUNNING|                                       |
| ✅ | nginx-app                        | RUNNING|                                       |
| 🔥| nginx-deployment-77fbdb7874-8gkcr| FAILING| Back-off pulling image "nginx2:1.16.1"|
| 🔥| nginx-deployment-77fbdb7874-8jps7| FAILING| Back-off pulling image "nginx2:1.16.1"|
| 🔥| nginx-deployment-77fbdb7874-vx27r| FAILING| Back-off pulling image "nginx2:1.16.1"|
| ✅ | postgres                         | RUNNING|                                       |
| ✅ | coredns-f9fd979d6-qwgfm          | RUNNING|                                       |
| ✅ | etcd-minikube                    | RUNNING|                                       |
| ✅ | kube-apiserver-minikube          | RUNNING|                                       |
| ✅ | kube-controller-manager-minikube | RUNNING|                                       |
| ✅ | kube-proxy-s5bcc                 | RUNNING|                                       |
| ✅ | kube-scheduler-minikube          | RUNNING|                                       |
| ✅ | storage-provisioner              | RUNNING|                                       |
$
```

---


### Conclusion

The Kubectl tool is easily extensible and Jbang allows to write scripts in Java in no time, where we would probably have turned to Python before that tool existed.

### References : 

- The code from this blog post : https://github.com/ikwattro/kubectl-plugin-java-jbang
- `kubectl` : plugins documentation : https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/
- `jbang` : https://github.com/jbangdev/jbang#installation
- `fabric8` : https://github.com/fabric8io/kubernetes-client