Adding Custom Extensions To The New (Quarkus) Keycloak Operator
In recent times the Keycloak Operator for OpenShift has moved from the old EAP
based implementation to a new implementation based on Quarkus. At the same time
the Custom Resource Definition for a Keycloak
object has moved from
apiVersion v1alpha1
to v2alpha1
.
While on average this is a good move, and it brings many improvements, some functionality appears to have not made the transition yet, most notably the ability to easily add custom extensions and providers.
Since one of our customers encountered this and asked for our help we went and sought a solution to this, without having to resort to building custom images, as those would add an extra maintenance burden on an already busy team.
The Solution
The solution is actually fairly straightforward: While the operator no
longer has a configuration option for extensions, Keycloak itself still happily
loads any extensions it finds on disk (from /opt/keycloak/extensions
in the
case of using the operator). The remaining problem now is to add the files
there without resorting to building custom images.
That second, smaller, problem is actually accounted for by using the
spec.unsupported.podTemplate
stanza in the Keycloak custom resource. Here you
can add a Kubernetes Pod Template that is merged with the default Pod Template
for your Keycloak Instances.
N.B. As the name of the stanza implies this is UNSUPPORTED. It works at the time of writing, it might work tomorrow or next month, but it is not guaranteed to keep on working forever.
An Example
Let’s put all of what we’ve discovered together:
- Create a project on OpenShift for our experiment:
1oc new-project keycloak-test
- Install the Keycloak Operator in this namespace:
- Create an OperatorGroup
- Create a Subscription
1oc apply -f - << EOF 2apiVersion: operators.coreos.com/v1alpha1 3kind: Subscription 4metadata: 5 name: keycloak-operator 6 namespace: keycloak-test 7spec: 8 channel: fast 9 installPlanApproval: Automatic 10 name: keycloak-operator 11 source: community-operators 12 sourceNamespace: openshift-marketplace 13EOF
- Download the extensions you wish to use, we are going to use “Apple Identity
Provider for Keycloak” extensions, since it’s easy to see if it’s loaded
from the main admin page.
1 wget https://github.com/klausbetz/apple-identity-provider-keycloak/releases/download/1.13.0/apple-identity-provider-1.13.0.jar
- Create a ConfigMap named
providers
, and fill it with the extension(s) you wish to use. - Create a new Keycloak instance
1oc create -f - << EOF 2apiVersion: k8s.keycloak.org/v2alpha1 3kind: Keycloak 4metadata: 5 labels: 6 app: sso 7 name: example-keycloak 8 namespace: keycloak-test 9spec: 10 hostname: 11 hostname: keycloak.apps-crc.testing 12 http: 13 tlsSecret: my-tls-secret 14 ingress: 15 annotations: 16 route.openshift.io/termination: reencrypt 17 instances: 1 18 startOptimized: false 19 unsupported: 20 podTemplate: 21 spec: 22 containers: 23 - volumeMounts: 24 - mountPath: /opt/keycloak/providers 25 name: keycloak-providers 26 volumes: 27 - configMap: 28 name: providers 29 name: keycloak-providers 30EOF
- The new Keycloak instance isn’t yet starting, since it can’t find the secret
my-tls-secret
, let’s remedy that1oc annotate service example-keycloak-service service.beta.openshift.io/serving-cert-secret-name=my-tls-secret
- Wait for the Keycloak pod to come up. Then login to the admin console as the
admin
user. You can get the password from thekeycloak-initial-admin
secretThe URL to login to is listed in your Keycloak CR as1oc get secret example-keycloak-initial-admin -o go-template='{{ .data.password | base64decode }}'
spec.hostname
. You will need to adjust that one to suit your installation. In our example we also changed the default route from “passthrough” TLS termination to “Reencrypt”, using the wildcard certificates on our OpenShift router. - If everything went well you should now have the “Apple” provider available
Improvements
In this example we have used a ConfigMap to store our extensions. While this works in this example, where the extension is fairly small, for larger or more extensions it might be better to switch to a PersistentVolumeClaim, and a Kubernetes Job to download extensions to that PVC. This is left as an exercise for the reader.