Prototyping a web app that can talk to Kubernetes API

June 29, 2020
kubernetes development frontend API JavaScript express.js

Motivation

With the advent of various types of operators on top of Kubernetes platform, it might be useful to provide users with a fancy dashboard. However, dashboards these days are often written in JavaScript and libraries/frameworks like React.js, Vue.js or Angular. Kubernetes API on the other hand is a bunch of REST endpoints. So it all may look pretty simple, just call those endpoints from the app written in JavaScript, right? Unfortunatelly, it’s not that simple, because of the AuthN and AuthZ that is by default turned on in the Kubernetes REST endpoints and giving the bearer token to the client-side JavaScript app (in browser) might be a security risk.

Let’s dive into the problem and let’s code some very simple web application in the following couple of blog posts.

Design

Here is the idea. We know that client-side JavaScript app will by definition run on the client side, but at the same time we need to be able to access the K8s api using the token. So let’s split the webapp into two pieces:

In other words the server-side rest endpoints will be transforming the incoming request to the request for the Kubernetes API. Because of the fact it will be deployed on a pod in k8s, it will have an access to the secrets with the CA certificate and baerer token representing the service account. So that RBAC can be controlled by Kubernetes itself. Our goal will be to list all the deployments in the cluster that have given label on them. This is a common pattern when working with operators, because it’s a good practice to label the resources it creates.

Part 1 - the server-side

Generate the project

We are lazy so we would like to write as little of code as possible. Let’s scaffold an example Express.js application using express-generator-typescript-k8s. It generates also the swagger specs (OpenAPI 3) and contains also swagger ui out of the box. It also comes with the preinstalled JavaScript linter, Kubernetes client, TypeScript and other goodies.

Install the generator:

$ npx express-generator-typescript-k8s --openAPI "rest-api"

Check what has been generated:

$ tree rest-api/src/
rest-api/src/
├── api.yaml
├── entities
│   └── Deployment.ts
├── index.ts
├── LoadEnv.ts
├── public
│   ├── scripts
│   │   └── index.js
│   └── stylesheets
│       └── style.css
├── routes
│   ├── Deployments.ts
│   └── index.ts
├── Server.ts
├── shared
│   ├── constants.ts
│   ├── functions.ts
│   ├── KubernetesClient.ts
│   └── Logger.ts
└── views
    └── index.html

You can run the application in development mode by npm run start:dev and explore the simple web ui on localhost:3000. Here is how it looks like (assuming you have an access to a running k8s cluster):

It lists the Deployment resources in Kubernetes, so basically same output as the kubectl get deployments --all-namespaces. If you open the dev console in the browser, you can find out that the web ui calls the rest endpoint /api/deployments/, we can also try the curl http://localhost:3000/api/deployments/. The webui is vanilla javascript and css and we will not be using it. We will write a new fancy UI in react.js that will be talking to the REST endpoint. The kubernetes client is used in the src/routes/Deployments.ts and here is a self-explanatory example:

...
/******************************************************************************
 *                      Get All Deployments - "GET /api/deployments/"
 ******************************************************************************/

router.get('/', async (req: Request, res: Response) => {
    const k8sDeployments = await k8sAppsApi.listDeploymentForAllNamespaces();
    const deployments = k8sDeployments.body.items.map(Deployment.parseDeployment);
    console.log(deployments);
    return res.status(OK).json({deployments});
});
...

This is a good starting point. Let’s extend the example REST API and add an endpoint that would list all our CustomResources.

Custom Resources in Kubernetes

A lot of content has been written on this topic so I’ll try to keep it brief and stick to our task. For more comprehensive overview on this topic, I suggest the k8s docs.

First let’s define our CRD to have something to work with or we can use a real one from an existing operator. It’s common in the JavaScript land to have the todo app as the hello world, so why not to use highly sophisticated distributed system that can schedule pods as a memo storage? :)

example CRD:

$ cat <<CRD | kubectl apply -f -
kind: CustomResourceDefinition
apiVersion: apiextensions.k8s.io/v1beta1
metadata:
  name: todos.jkremser.github.io
spec:
  group: jkremser.github.io
  names:
    kind: Todo
    listKind: TodoList
    plural: todos
    singular: todo
  scope: Namespaced
  version: v1
  additionalPrinterColumns:
  - name: Text
    type: string
    description: The task to be done.
    JSONPath: .spec.text
  - name: Done
    type: boolean
    JSONPath: .spec.done
CRD

Now we can query the kubernetes for Todo resources:

$ kubectl get todos
No resources found in default namespace.

and create them:

$ cat <<CR | kubectl apply -f -
apiVersion: jkremser.github.io/v1
kind: Todo
metadata:
  name: finish-this-blogpost
spec:
  text: "Enhance the content with more self-referential stuff."
  done: false
CR

$ kubectl get todos
NAME                   TEXT                                                    DONE
finish-this-blogpost   Enhance the content with more self-referential stuff.   false

Add REST Endpoint

Now, that we are able to CRUD our Todos in Kubernetes using the CLI client, let’s extend the REST api to do the same.

To follow the project structure, we will create a new router for express.js server called src/routes/Todos.ts with the following content:

example for ‘get all’ (full version [here](todo: link na github)):

/******************************************************************************
 *                      Get All Todos - "GET /api/todos/"
 ******************************************************************************/

router.get('/', async (req: Request, res: Response) => {
    const crdClient: CustomObjectsApi = k8sKubeConfig.makeApiClient(CustomObjectsApi);

    const k8sTodos: any = await crdClient.listNamespacedCustomObject('jkremser.github.io', 'v1', 'default', 'todos');
    const todos = k8sTodos.body.items.map(Todo.parse);
    console.log(todos);
    return res.status(OK).json({todos});
});

We also need to register the new router in src/routes/index.ts and create the entity called Todo, clone the git repo for working version.

After restarting the app, we should be able to call the API:

\o/

Conclusion

We have a working REST endpoints that provide access to our custom resources in K8s cluster. This concludes the part 1. Stay tuned for part 2.

comments powered by Disqus