Discovering OpenShift Resources in Quarkus

I have a product-catalog application that I have been using as a demo for awhile now, it’s essentially a three tier application as per the topology view below with the front-end (client) using React, the back-end (server) written in Quarkus and a Maria database.

The client application is a Single Page Application (SPA) using React that talks directly to the server application via REST API calls. As a result, the Quarkus server back-end needs to have CORS configured in order to accept requests from the front-end application. While a wildcard, i.e. ‘*’, certainly works, in cases where it’s not a public API I prefer a more restrictive setting for CORS, i.e. http://client-product-catalog-dev.apps.home.ocplab.com.

The downside of this restrictive approach is that I need to customize this CORS setting on every namespace and cluster I deploy the application into since the client route is unique in each of those cases. While tools like kustomize or helm can help with this, the client URL needed for the CORS configuration is already defined as a route in OpenShift so why not just have the application discover the URL at runtime via the kubernetes API?

This was my first stab at using the openshift-client in Quarkus and it was surprisingly easy to get going. The Quarkus guide on using the kubernetes/openshift client is excellent as is par for the course with Quarkus guides. Folowing the guide, the first step is adding the extension to your pom.xml:

./mvnw quarkus:add-extension -Dextensions="openshift-client"

After that it’s just a matter of writing some code to discover the route. I opted to label the route with endpoint:client and to search for the route by that label. The first step was to create a LabelSelector as follows:

LabelSelector selector = new LabelSelectorBuilder().withMatchLabels(Map.ofEntries(entry("endpoint", "client"))).build();

Now that we have the label selector we can then ask for a list of routes matching that selector:

List<Route> routes = openshiftClient.routes().withLabelSelector(selector).list().getItems();

Finally with the list of routes I opt to use the first match. Note for simplicity I’m omitting a bunch of checking and logging that I am doing if there are zero matches or multiple matches, the full class with all of those checks appears further below.

Route route = routes.get(0);
String host = route.getSpec().getHost();
boolean tls = false;
if (route.getSpec().getTls() != null && "".equals(route.getSpec().getTls().getTermination())) {
    tls = true;
}
String corsOrigin = (tls?"https":"http") + "://" + host;

Once we have our corsOrigin, we set it as a system property to override the default setting:

System.setProperty("quarkus.http.cors.origins", corsOrigin);

In OpenShift you will need to give the view role to the serviceaccount that is running the pod in order for it to be able to interact with the Kubernetes API. This can be done via the CLI as follows:

oc adm policy add-role-to-user view -z default

Alternatively if using a kustomize or GitOps the equivalent yaml would be as follows:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: default-view
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: view
subjects:
- kind: ServiceAccount
  name: default

So that’s basically it, with a little bit of code I’ve reduced the amount of configuration that needs to be done to deploy the app on a per namespace/cluster basis. The complete code is below appears below.

package com.redhat.demo;
 
import java.util.Map;
import static java.util.Map.entry;
 
import java.util.List;
 
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
 
import io.fabric8.kubernetes.api.model.LabelSelector;
import io.fabric8.kubernetes.api.model.LabelSelectorBuilder;
import io.fabric8.openshift.api.model.Route;
import io.fabric8.openshift.client.OpenShiftClient;
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
 
import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.logging.Logger;
 
@ApplicationScoped
public class OpenShiftSettings {
 
    private static final Logger LOGGER = Logger.getLogger("ListenerBean");
 
    @Inject
    OpenShiftClient openshiftClient;
 
    void onStart(@Observes StartupEvent ev) {
        // Test if we are running in a pod
        String k8sSvcHost = System.getenv("KUBERNETES_SERVICE_HOST");
        if (k8sSvcHost == null || "".equals(k8sSvcHost)) {
            LOGGER.infof("Not running in kubernetes, using CORS_ORIGIN environment '%s' variable",
                    ConfigProvider.getConfig().getValue("quarkus.http.cors.origins", String.class));
            return;
        }
 
        if (System.getenv("CORS_ORIGIN") != null) {
            LOGGER.infof("CORS_ORIGIN explicitly defined bypassing route lookup");
            return;
        }
 
        // Look for route with label endpoint:client
        if (openshiftClient.getMasterUrl() == null) {
            LOGGER.info("Kubernetes context is not available");
        } else {
            LOGGER.infof("Application is running in OpenShift %s, checking for labelled route",
                    openshiftClient.getMasterUrl());
 
            LabelSelector selector = new LabelSelectorBuilder()
                    .withMatchLabels(Map.ofEntries(entry("endpoint", "client"))).build();
            List<Route> routes = null;
            try {
                routes = openshiftClient.routes().withLabelSelector(selector).list().getItems();
            } catch (Exception e) {
                LOGGER.info("Unexpected error occurred retrieving routes, using environment variable CORS_ORIGIN", e);
                return;
            }
            if (routes == null || routes.size() == 0) {
                LOGGER.info("No routes found with label 'endpoint:client', using environment variable CORS_ORIGIN");
                return;
            } else if (routes.size() > 1) {
                LOGGER.warn("More then one route found with 'endpoint:client', using first one");
            }
 
            Route route = routes.get(0);
            String host = route.getSpec().getHost();
            boolean tls = false;
            if (route.getSpec().getTls() != null && "".equals(route.getSpec().getTls().getTermination())) {
                tls = true;
            }
            String corsOrigin = (tls ? "https" : "http") + "://" + host;
            System.setProperty("quarkus.http.cors.origins", corsOrigin);
        }
        LOGGER.infof("Using host %s for cors origin",
                ConfigProvider.getConfig().getValue("quarkus.http.cors.origins", String.class));
    }
 
    void onStop(@Observes ShutdownEvent ev) {
        LOGGER.info("The application is stopping...");
    }
}

This code is also in my public repository.