Skip to content

Commit

Permalink
Support for installing Keycloak Operator via OLM. (#768)
Browse files Browse the repository at this point in the history
Inludes workaround for issue #28638.

Signed-off-by: Tomas Kyjovsky <[email protected]>
  • Loading branch information
tkyjovsk authored Apr 15, 2024
1 parent 9eed4b7 commit 7b1c8e5
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,30 @@ Cryostat depends on the Cryostat Operator to be installed, which is the default

To configure the deployment, see xref:customizing-deployment.adoc[] for details.

=== Installing Keycloak from the Operator Hub

By default the operator is installed directly via https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/refs/tags/nightly/kubernetes/kubernetes.yml[`keycloak-k8s-resources`].

To install the operator from the _Operator Hub_ using the _Operator Lifecycle Manager_ set:
[source,shell]
----
KC_OPERATOR_OLM=true
----

Additionally it is possible to customize the following parameters:

KC_OPERATOR_CATALOG_SOURCE_NS:: Namespace of the OLM catalog source. Defaults to `openshift-marketplace`.
KC_OPERATOR_CATALOG_SOURCE:: OLM catalog source. Defaults to `community-operators`.
KC_OPERATOR_NAME:: Name of the Keycloak Operator in the catalog source. Defaults to `keycloak-operator`.
KC_OPERATOR_CHANNEL:: OLM subscription channel. If not set the default channel of the selected operator will be used.
KC_OPERATOR_VERSION:: Keycloak Operator version. If not set the current version from the selected channel will be used.
+
Note that the actual _Cluster Service Version_ used for OLM subscription is set by convention as:
+
----
${KC_OPERATOR_NAME}.v${KC_OPERATOR_VERSION}
----

=== Verifying the installation

Run the following script to check if all services are running and receive a list of available URLs:
Expand Down
19 changes: 19 additions & 0 deletions provision/keycloak-tasks/Utils.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,25 @@ tasks:
- if [ ! -d .task ]; then mkdir .task; fi
- echo "image-registry.openshift-image-registry.svc:5000/{{.NAMESPACE}}/keycloak-with-aws-jdbc-wrapper:latest" > .task/var-CUSTOM_CONTAINER_IMAGE_FILE

install-keycloak-operator-olm:
desc: "Install the Keycloak operator via the Operator Lifecycle Manager"
internal: true
requires:
vars:
- NAMESPACE
- KUBECONFIG
preconditions:
- test -f {{.KUBECONFIG}}
cmds:
- KUBECONFIG="{{.KUBECONFIG}}" kubectl create namespace "{{.NAMESPACE}}" || true
- INSTALL_NAMESPACE="{{.NAMESPACE}}" ./olm.sh
env:
CATALOG_SOURCE_NAMESPACE: '{{ default "openshift-marketplace" .KC_OPERATOR_CATALOG_SOURCE_NS }}'
CATALOG_SOURCE: '{{ default "community-operators" .KC_OPERATOR_CATALOG_SOURCE }}'
PRODUCT: '{{ default "keycloak-operator" .KC_OPERATOR_NAME }}'
CHANNEL: '{{ .KC_OPERATOR_CHANNEL }}'
VERSION: '{{ .KC_OPERATOR_VERSION }}'

install-keycloak-operator:
desc: "Install the Keycloak operator"
internal: true
Expand Down
133 changes: 133 additions & 0 deletions provision/keycloak-tasks/olm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/bin/bash -e
#
# Script for installing Keycloak Operator via the Operator Lifecycle Manager.
#
# Assuming OLM framework is installed, this script will create an OperatorGroup and a Subscription resources
# based on the supplied parameters.
#
# INSTALL_NAMESPACE - Target namespace for the installation. Required.
# CATALOG_SOURCE_NAMESPACE - Namespace of the catalog source, e.g. `openshift-marketplace`. Required.
# CATALOG_SOURCE - Name of the catalog source, e.g. `community-operators`. Required.
# PRODUCT - Name of the product, e.g. `keycloak-operator`. Required.
# CHANNEL - Update channel for the selected product. Optional. If not set the default channel for the product will be used.
# CSV - Cluster Service Version of the product. Optional.
# If CSV is not set and parameter VERSION is set then CSV will be set as "${PRODUCT}.v${VERSION}".
# If neither CSV nor VERSION are set then the default CSV for the selected channel will be used.
#
# Afterwards the script will check for an install plan created by the subscription
# and if it matches the requested version it will automatically approve the plan.
#
# Note that upgrading an existing intallation across multiple versions is NOT SUPPORTED
# because OLM will sequentially create intall plans for all intermediate versions
# whereas this script will only specifically approve the requested version.
#
# Also note that OLM doesn't support downgrades. In these cases it is necessary
# to install from scratch.
#

function requireVariableToBeSet { if [ -z "${!1}" ]; then echo "ERROR: Variable $1 is not set." >&2; exit 1; fi }

requireVariableToBeSet INSTALL_NAMESPACE
requireVariableToBeSet CATALOG_SOURCE_NAMESPACE
requireVariableToBeSet CATALOG_SOURCE
requireVariableToBeSet PRODUCT

export INSTALL_NAMESPACE
export CATALOG_SOURCE_NAMESPACE
export CATALOG_SOURCE
export PRODUCT

echo "Looking up package manifest for product \"$PRODUCT\" in catalog \"$CATALOG_SOURCE\", namespace \"$CATALOG_SOURCE_NAMESPACE\"."
packageManifest=$(kubectl -n "$CATALOG_SOURCE_NAMESPACE" get packagemanifests --field-selector=metadata.name=$PRODUCT -ojson | jq -r ".items[] | select (.status.catalogSource==\"$CATALOG_SOURCE\")")
if [ -z "$packageManifest" ]; then echo "ERROR: Package manifest not found."; exit 1; fi

if [ -z "$CHANNEL" ]; then
echo "Parameter CHANNEL not provided. Looking up the default channel."
CHANNEL=$(echo "$packageManifest" | jq -r .status.defaultChannel)
if [ -z "$CHANNEL" ]; then echo "ERROR: Default channel not found."; exit 1; fi
echo "Default channel is: \"$CHANNEL\""
fi
export CHANNEL

if [ -z "$CSV" ]; then
if [ ! -z "$VERSION" ]; then
echo "Parameter CSV not provided. Setting based on PRODUCT and VERSION: \"${PRODUCT}.v${VERSION}\""
CSV="${PRODUCT}.v${VERSION}"
else
echo "Parameter CSV or VERSION not provided. Looking up current CSV for channel \"$CHANNEL\"."
CSV=$(echo "$packageManifest" | jq -r ".status.channels[] | select(.name==\"${CHANNEL}\") .currentCSV" )
if [ -z "$CSV" ]; then echo "Error looking up current CSV."; exit 1; fi
echo "Current CSV for channel \"$CHANNEL\" is: \"$CSV\""
fi
fi
export CSV

export OPERATOR_GROUP_NAME=${PRODUCT}

og=$(cat olm/templates/operatorgroup.yaml | envsubst)
sub=$(cat olm/templates/subscription.yaml | envsubst)
echo
echo "${og}"
echo
echo "${sub}"
echo

echo "Checking whether subscription with the requested CSV already exists."
if kubectl -n $INSTALL_NAMESPACE wait subscriptions/$PRODUCT --for=jsonpath='.status.installedCSV'=${CSV} --timeout=10s; then
echo "Subscription with the requested CSV already exists. Skipping."
else
echo "Subscription with the requested CSV not found. Applying resources."
echo "${og}" | kubectl apply -f -
echo "${sub}" | kubectl apply -f -

echo "Checking for pending install plans."
if kubectl -n $INSTALL_NAMESPACE wait subscriptions/$PRODUCT --for=condition=InstallPlanPending --timeout=60s; then
installPlanName=$(kubectl -n $INSTALL_NAMESPACE get subscriptions/$PRODUCT -ojson | jq -r .status.installPlanRef.name)
installPlanCSV=$(kubectl -n $INSTALL_NAMESPACE get ip/${installPlanName} -ojson | jq -r '.spec.clusterServiceVersionNames[0]')
echo "Subscription references install plan \"${installPlanName}\" with CSV \"${installPlanCSV}\"."
if [[ "$installPlanCSV" == "$CSV" ]]; then
echo "Install plan CSV matches the requested version. Approving."
kubectl -n $INSTALL_NAMESPACE patch ip/${installPlanName} --patch '{"spec":{"approved":true}}' --type=merge
else
echo "ERROR: CSV doesn't match the requested CSV: \"$CSV\". Install plan NOT APPROVED."
exit 2
fi
echo "Validating installed CSV."
kubectl -n $INSTALL_NAMESPACE wait subscriptions/$PRODUCT --for=jsonpath='.status.installedCSV'=${CSV} --timeout=60s
else
echo "ERROR: No pending install plans were found."
echo "Subscription status:"
kubectl -n $INSTALL_NAMESPACE get subscriptions/$PRODUCT -ojson | jq .status.conditions
exit 1
fi
fi

if [[ "$KC_WORKAROUND_28638" == "true" ]]; then
echo "Applying post-install workaround for issue #28638 https://github.com/keycloak/keycloak/issues/28638"

echo " looking up keycloak-operator-role for \"$CSV\""
attempts=30
for a in $(seq $attempts); do
keycloakOperatorRole=$(kubectl -n $INSTALL_NAMESPACE get role -l olm.owner=$CSV -ojson | jq -r '.items[0].metadata.name')
if [[ $keycloakOperatorRole == ${PRODUCT}* ]]; then
echo " found: $keycloakOperatorRole"
break;
elif [ $a -ge $attempts ]; then
echo " ERROR: Role not found after $attempts attempts."
exit 3
fi
sleep 1
done

echo " adding configmaps permissions to the role"
kubectl -n $INSTALL_NAMESPACE get role $keycloakOperatorRole -o json \
| jq '.rules += [{"apiGroups":[""],"resources":["configmaps"],"verbs":["get","list","watch"]}]' \
| kubectl apply -f -

echo " looking up operator pod"
operatorPod=$(kubectl -n $INSTALL_NAMESPACE get pods -oname | grep $PRODUCT | head -1)
echo " found: $operatorPod, rebooting"
kubectl -n $INSTALL_NAMESPACE delete $operatorPod

echo "Workaround applied."
fi
8 changes: 8 additions & 0 deletions provision/keycloak-tasks/olm/templates/operatorgroup.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
kind: OperatorGroup
apiVersion: operators.coreos.com/v1
metadata:
name: ${OPERATOR_GROUP_NAME}
namespace: ${INSTALL_NAMESPACE}
spec:
targetNamespaces:
- ${INSTALL_NAMESPACE}
12 changes: 12 additions & 0 deletions provision/keycloak-tasks/olm/templates/subscription.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
kind: Subscription
apiVersion: operators.coreos.com/v1alpha1
metadata:
name: ${PRODUCT}
namespace: ${INSTALL_NAMESPACE}
spec:
sourceNamespace: ${CATALOG_SOURCE_NAMESPACE}
source: ${CATALOG_SOURCE}
name: ${PRODUCT}
channel: ${CHANNEL}
startingCSV: ${CSV}
installPlanApproval: Manual
2 changes: 1 addition & 1 deletion provision/openshift/Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ tasks:
vars:
NAMESPACE: "{{.KC_NAMESPACE_PREFIX}}keycloak"
ROSA_CLUSTER_NAME: "current"
- task: keycloak:install-keycloak-operator
- task: keycloak:install-keycloak-operator{{ if .KC_OPERATOR_OLM }}-olm{{ end }}
vars:
NAMESPACE: "{{.KC_NAMESPACE_PREFIX}}keycloak"
- >
Expand Down
2 changes: 1 addition & 1 deletion provision/openshift/isup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ for SERVICE in "${!SERVICES[@]}"; do

if [[ "${SERVICE}" == "keycloak-${KC_NAMESPACE_PREFIX}keycloak.${KC_HOSTNAME_SUFFIX}" || "${SERVICE}" == "${KC_HEALTH_HOSTNAME}" ]]
then
kubectl wait --for=condition=Available --timeout=1200s deployments.apps/keycloak-operator -n "${KC_NAMESPACE_PREFIX}keycloak"
kubectl wait --for=condition=Available --timeout=1200s deployments.apps/${KC_OPERATOR_NAME:-keycloak-operator} -n "${KC_NAMESPACE_PREFIX}keycloak"
kubectl wait --for=condition=Ready --timeout=1200s keycloaks.k8s.keycloak.org/keycloak -n "${KC_NAMESPACE_PREFIX}keycloak"
kubectl wait --for=condition=RollingUpdate=False --timeout=1200s keycloaks.k8s.keycloak.org/keycloak -n "${KC_NAMESPACE_PREFIX}keycloak"
fi
Expand Down

0 comments on commit 7b1c8e5

Please sign in to comment.