The IBM Stock Trader operator, part 3: UI integration and final thoughts

John Alcorn
AI+ Enterprise Engineering
9 min readMay 14, 2020

Learn how to integrate your operator into the OpenShift console UI

In part 1, you learned how to use the IBM Stock Trader operator to deploy and configure the sample. In part 2, you learned how to develop such an operator. Now, let’s look at the last remaining piece — how to integrate it into the OpenShift console.

There’s one more use of the Operator SDK, which is optional, and only needed if you want your operator to appear under Installed Operators in the OpenShift console. You need to generate the ClusterServiceVersion (CSV), by running the following command:

operator-sdk generate csv --csv-version 0.1.0

This adds an olm-catalog subdirectory under the deploy directory. This will contain a directory structure with a couple of yaml files, the most important being the CSV yaml. Let’s look at the first few lines of that generated file, which is quite large in total (containing your entire values.yaml, among other things).

apiVersion: operators.coreos.com/v1alpha1
kind: ClusterServiceVersion
metadata:
name: stocktrader-operator.v0.1.0
annotations:
description: Umbrella operator that installs and configures the IBM Stock Trader
capabilities: Basic Install
createdAt: 2020-01-31 16:00:00
support: IBM
containerImage: ibmstocktrader/stocktrader-operator:latest
repository: https://github.com/IBMStockTrader/stocktrader-operator
alm-examples: |-
[
{
"apiVersion": "operators.ibm.com/v1",
"kind": "StockTrader",
"metadata": {
"name": "example-stocktrader"
},
"spec": {
"database": {
"kind": "db2",
"db": "trader",
"host": "db2trader1-ibm-db2oltp-dev",
"id": "db2inst1",
"password": "db2inst1",
"port": 50000
},

The first thing to notice, on line 1, is that this UI integration stuff is still Alpha level quality. It has NOT yet reached Beta, much less a v1 level. So all of the discussion that follows on the CSV in this article is subject to change in the future, at Red Hat’s discretion.

Looking on down, you’ll see when it was created, who supports it, where the built container image is to be found, and where the source code is to be found. In my case, I created it in late January, I support it (feel free to open issues), the Docker image is in DockerHub, and the source code is in GitHub.

Then you’ll see the alm-examples section. This is where you specify the default values that should be shown for each field of the CR that the user is creating. This section is all generated by the SDK, so you don’t need to worry about it. Note it’s basically just a copy of your values.yaml from your helm chart; so if you update your values.yaml, like to add an additional field, you’ll need to make the corresponding updates here (it’s share-by-copy, not share-by-reference, which is kind of disappointing — easy to get out of synch).

There’s also a copy of the operator.yaml in here, containing the Deployment definition for the operator, including where to grab the image from, and the env vars it expects, in the install section. And there’s a copy of the role.yaml we discussed in part 2 embedded in here, in the permissions section. Again — you’ll need to manually synch these sections with any changes you make to the original files, which is unfortunate.

Let’s also look at the final few lines of the CSV yaml file (saving the large middle section for last), starting at line 1004 in the current version in GitHub. It has the following contents:

installModes:
- supported: true
type: OwnNamespace
- supported: true
type: SingleNamespace
- supported: false
type: MultiNamespace
- supported: true
type: AllNamespaces
maturity: alpha
maintainers:
- name: John W. Alcorn
email: jalcorn@us.ibm.com
provider:
name: IBM
links:
- name: IBM Stock Trader on GitHub
url: https://github.com/IBMStockTrader
- name: Cloud Engagement Hub on Medium
url: https://medium.com/cloud-engagement-hub
- name: Cloud Native Programming Model
url: http://ibm.biz/ProgModel
icon:
- base64data: <icon data>
mediatype: image/png
replaces: stocktrader-operator.v0.0.0
version: 0.1.0

As you can see, you get to specify whether you want it to work only in the same namespace where it is deployed, in a single separate namespace, in multiple namespaces (which rumor says is likely to get deprecated/removed), or in all namespaces. I like the idea of keeping the namespace hosting the operator separate from the namespace where an instance of the CR it produces is deployed (I think of the operator itself as an extension to Kubernetes — not something for which end-users should see its pod in their namespace), so AllNameSpaces is my favorite.

You also get to specify the maturity level of the operator, your contact links, and links to any URLs you want to publicize with the operator (I included links to 3 things that I own/co-author).

You also get to specify an icon. Rather than just giving a URL to where to find it, like you do with a Helm chart, you have to base64-encode your icon png file and put the results here (which results in a line that is tens of thousands of characters long, which I dislike). On my Mac, I ran the following:

base64 -i stock-trader-icon.png -o encoded.txt

And then copied the contents of that encoded.txt to where you see <icon data> in the CSV yaml snippet above.

OK, now let’s talk about what you add to the CSV yaml file for each field in each section you want to appear in the form UI in the console. This is the confusing/tedious part, which is NOT generated for you by the SDK, and which frankly isn’t that well documented. Again, I was helped immensely by being able to look at the open-sourced Open Liberty operator’s CSV — though I do some fancy UI things beyond what it does (if I do say so myself… lol). Here are the first few lines of my specDescriptors stanza, showing my global section:

specDescriptors:
# Section for global settings
- path: global.auth
description: Choose from Basic, LDAP, or OIDC
displayName: Select the type of authentication
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:select:basic
- urn:alm:descriptor:com.tectonic.ui:select:ldap
- urn:alm:descriptor:com.tectonic.ui:select:oidc
- urn:alm:descriptor:com.tectonic.ui:fieldGroup:global
- path: global.replicas
description: Initial number of pods per microservice
displayName: Replicas
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:podCount
- urn:alm:descriptor:com.tectonic.ui:fieldGroup:global
- path: global.autoscale
description: Enable a Horizontal Pod Autoscaler for each microservice
displayName: Enable Auto-scaling
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:booleanSwitch
- urn:alm:descriptor:com.tectonic.ui:fieldGroup:global

As you can see, you make a stanza for each field, giving the section.field path to that field within your CRD/values.yaml, along with the display name and description you’d like shown in the console for each field. This is roughly equivalent to the info I used to put in my values-metadata.yaml file in my helm chart, for the old IBM Cloud Private console’s helm UI panels (that was an IBM-proposed extension to Helm which never caught on outside of IBM).

You’ll also see the x-descriptors section for each field. Here, you get to pick the Tectonic UI widgets you’d like used to visualize the fields in the OpenShift console. After some googling, and looking through various GitHub issues and StackOverflow posts, I discovered that the acceptable values are described at https://github.com/openshift/console/blob/master/frontend/packages/operator-lifecycle-manager/src/components/descriptors/reference/reference.md (notice the fieldGroup descriptor — fields with the same value after the colon will show in the same expandable/collapsible twisty section, so these will all show under a section named global; notice also the booleanSwitch widget used for a boolean, the podCount widget used to pick how many pods you want by default, and the select widget to show values of an enum).

Let’s look at one more section, to see some additional Tectonic UI descriptors. Here’s the Watson section:

  # Section for Watson settings
- path: watson.id
description: Login ID for the Watson Tone Analyzer. Leave set to `apikey` if using an API key instead
displayName: ID
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:text
- urn:alm:descriptor:com.tectonic.ui:fieldGroup:watson
- urn:alm:descriptor:com.tectonic.ui:advanced
- path: watson.passwordOrApiKey
description: Login password for the Watson Tone Analyzer. If you have an API key instead, specify that here
displayName: Password or API Key
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:password
- urn:alm:descriptor:com.tectonic.ui:fieldGroup:watson
- urn:alm:descriptor:com.tectonic.ui:advanced
- path: watson.url
description: URL for the Watson Tone Analyzer. Generally the default value will be sufficient
displayName: URL
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:text
- urn:alm:descriptor:com.tectonic.ui:fieldGroup:watson
- urn:alm:descriptor:com.tectonic.ui:advanced

As you can see, there are Tectonic UI widgets for text, and for password (which causes an asterisk to be shown for each character typed). And there’s an advanced descriptor, which causes the fieldGroup section to be initially collapsed (good for optional sections you don’t want to show unless the user specifically clicks on it to see). All of this helps your operator conform to the look and feel of the rest of the OpenShift console.

The optional Watson section expanded

OK, that’s it for the CSV. As described in part 1, you can just run an oc create -f against the CSV yaml file to make it show up under Installed Operators, since I don’t have it integrated into a real operator registry yet.

Before closing out, there are a couple of other issues I experienced/learned about, that I’d like to share here. The first is the fact that any resources created by my operator are not editable. Or to be more precise, you can edit them and the console will say it saved your changes (say, to the config map I generate), but then it mysteriously gets changed back to the original value a second or so later. At first I thought I was hitting some bug in OpenShift or its underlying Kubernetes. But after Slacking with the owners of the Open Liberty operator, I learned that this is to be expected.

Operators get the opportunity to be involved in “reconciliation” of any changes made by the user (or by the system) to any resources it created. If you had written your operator in Go, you could contribute logic for what changes you want to accept, and which to reject. For example, the Open Liberty operator accepts changes to labels and annotations, but not to other fields of their CRD for OpenLibertyApplication. That being said, my operator is written in Helm, and there’s no way to be involved in the “reconciliation” logic, since a Helm-based operator is “code-less” (it’s just yaml files). So the rule is, what the helm chart wants wins — or in other words, any changes the user tries to make to the generated resources get rejected, which is unfortunate.

The idea is, users should edit the CR they created (like the blog CR of type StockTrader that I described in part 2), not the resources generated from that; so if you want to update an expired password, you do it in the instance of the StockTrader you created, and then that will push the changes down to the generated resources, like the Secret where I store such credentials.

A workaround I’ve used (suggested by my colleague Greg Hintermeister) is to scale the number of pods for the operator down to zero. Then nothing is running to get involved in reconciliation, so the user wins. At least, temporarily — if you ever scale the operator back up, it will immediately revert everything you’d changed back to the original state.

One last issue I’ll mention, is that I’ve heard rumors that the Operator SDK is likely to get somewhat de-emphasized in the future, with many operator developers moving to KubeBuilder instead. However, this tool assumes you are going to write your operator in Go. As I said earlier, I chose to create a Helm-based operator since I’d already invested a lot of time in a helm chart. But for people that don’t have such “legacy” stuff they want to leverage, I’d recommend they follow the Go route. You can install KubeBuilder via:

brew install kubebuilder

Note that the directory structure of files generated by KubeBuilder is different than what the Operator SDK produces. Here’s a handy chart I saw in a presentation from a colleague recently explaining some of the differences:

Comparing Kubebuilder to the Operator SDK

I may decide to rewrite my Operator in Go (using KubeBuilder) in the future, since that would be a good learning experience, and I’d have more power to influence decisions, like around reconciliation of changes to generated resources. The two big efforts so far really were getting the CRD and the CSV right, and all of that is needed even if using Go, so I don’t feel like I’ve wasted time checkpointing at a Helm-based operator for now. Also, the tools, and the OLM framework itself in OpenShift, are all still very much in flux, so I’ll likely wait for stuff to at least reach Beta before diving back in.

And with that, I’d like to thank you for sticking with me throughout this series of articles. Hopefully this series has shown both how to use the operator I created, and how to create your own, including integrating it into the OpenShift console UI. Feel free to leave any questions/comments here, and stay tuned to our Cloud Engagement Hub channel for future posts.

--

--

John Alcorn
AI+ Enterprise Engineering

Member of the Cloud Journey Optimization Team at Kyndryl. Usually busy writing/testing code, or teaching others what I’ve learned.