diff --git a/Dockerfile b/Dockerfile
index 8f7ad429..752181fd 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,7 @@
FROM icr.io/appcafe/open-liberty:kernel-slim-java11-openj9-ubi-minimal
ARG VERSION=1.0
ARG REVISION=SNAPSHOT
+ARG SKIP_LINPERF=false
LABEL \
org.opencontainers.image.authors="Alasdair Nottingham" \
@@ -15,13 +16,20 @@ LABEL \
summary="Sample app running on Open Liberty that uses Eclipse MicroProfile" \
description="This image contains a sample application that displays the Java system properties and demonstrates MicroProfile Config, Health and Metrics."
-# Install required packages to run linperf.sh
+# Install required packages for running the Liberty MustGather (linperf.sh) script.
USER 0
-RUN command -v yum && pkgcmd=yum || pkgcmd=microdnf && ($pkgcmd update -y && $pkgcmd install -y procps-ng net-tools ncurses hostname)
+RUN if [ "$SKIP_LINPERF" != "true" ]; then \
+ PKG_MGR=$(command -v dnf || command -v microdnf) && \
+ $PKG_MGR install -y procps-ng net-tools ncurses hostname && \
+ $PKG_MGR clean all; \
+ fi
USER 1001
COPY --chown=1001:0 src/main/liberty/config/ /config/
+# Bundle the dev SSO for mpMetrics. You can override at deploy time by mounting a different sso.xml
+COPY --chown=1001:0 dev/sso.xml /config/configDropins/overrides/sso.xml
+
RUN features.sh
COPY --chown=1001:0 target/*.war /config/apps/
diff --git a/dev/getting-started-app.yaml b/dev/getting-started-app.yaml
new file mode 100644
index 00000000..6acf08ef
--- /dev/null
+++ b/dev/getting-started-app.yaml
@@ -0,0 +1,68 @@
+---
+# Development OpenLibertyApplication: Liberty app + Keycloak sidecar
+# Containers in the same Pod share localhost — Keycloak is reachable at localhost:8080.
+#
+# Prerequisites:
+# oc create configmap keycloak-realm --from-file=realm.json=dev/keycloak-realm.json
+# oc apply -f dev/getting-started-app.yaml
+#
+# To hit the /metrics endpoint run:
+# ./dev/metrics.sh
+#
+# In production: remove sidecarContainers, and provide a Secret
+# containing the real sso.xml mounted at /config/configDropins/overrides/sso.xml.
+apiVersion: apps.openliberty.io/v1
+kind: OpenLibertyApplication
+metadata:
+ name: getting-started
+spec:
+ applicationImage: icr.io/appcafe/open-liberty/samples/getting-started:latest
+ replicas: 1
+
+ service:
+ port: 9443
+
+ # Dev Keycloak sidecar — shares localhost with the Liberty container
+ sidecarContainers:
+ - name: keycloak
+ image: quay.io/keycloak/keycloak:25.0
+ args: ["start-dev", "--import-realm"]
+ env:
+ - name: KEYCLOAK_ADMIN
+ value: admin
+ - name: KEYCLOAK_ADMIN_PASSWORD
+ value: admin
+ volumeMounts:
+ - name: realm-config
+ mountPath: /opt/keycloak/data/import
+ ports:
+ - containerPort: 8080
+
+ # Mount the dev sso.xml; in production replace with a Secret mount pointing at your real IdP
+ volumeMounts:
+ - name: sso-override
+ mountPath: /config/configDropins/overrides/sso.xml
+ subPath: sso.xml
+
+ volumes:
+ - name: realm-config
+ configMap:
+ name: keycloak-realm
+ - name: sso-override
+ configMap:
+ name: liberty-sso-dev
+---
+# ConfigMap holding the dev sso.xml — points mpJwt at the Keycloak sidecar on localhost
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: liberty-sso-dev
+data:
+ sso.xml: |
+
+
+
+
+
diff --git a/dev/keycloak-realm.json b/dev/keycloak-realm.json
new file mode 100644
index 00000000..fec6432d
--- /dev/null
+++ b/dev/keycloak-realm.json
@@ -0,0 +1,44 @@
+{
+ "realm": "liberty",
+ "enabled": true,
+ "requiredCredentials": ["password"],
+ "attributes": {
+ "userProfileEnabled": "false"
+ },
+ "requiredActions": [
+ {
+ "alias": "VERIFY_PROFILE",
+ "name": "Verify Profile",
+ "providerId": "VERIFY_PROFILE",
+ "enabled": false,
+ "defaultAction": false,
+ "priority": 90,
+ "config": {}
+ }
+ ],
+ "clients": [
+ {
+ "clientId": "metrics-client",
+ "enabled": true,
+ "publicClient": false,
+ "secret": "metrics-secret",
+ "directAccessGrantsEnabled": true,
+ "defaultClientScopes": ["profile", "email"]
+ }
+ ],
+ "users": [
+ {
+ "username": "metrics",
+ "enabled": true,
+ "emailVerified": true,
+ "requiredActions": [],
+ "credentials": [
+ {
+ "type": "password",
+ "value": "metrics",
+ "temporary": false
+ }
+ ]
+ }
+ ]
+}
diff --git a/dev/metrics.sh b/dev/metrics.sh
new file mode 100755
index 00000000..d43d9905
--- /dev/null
+++ b/dev/metrics.sh
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+# dev/metrics.sh — fetch the mpMetrics endpoint from the getting-started pod
+#
+# Usage:
+# ./dev/metrics.sh # all metrics (default)
+# ./dev/metrics.sh /metrics/base # base metrics
+# ./dev/metrics.sh /metrics/application # application metrics
+
+set -uo pipefail # no -e: port-forward drops are handled explicitly
+
+METRICS_PATH="${1:-/metrics}"
+LABEL="app.kubernetes.io/name=getting-started"
+LIBERTY_PORT=9443
+KEYCLOAK_PORT=8080
+PF_PID=""
+
+cleanup() { [[ -n "$PF_PID" ]] && kill "$PF_PID" 2>/dev/null; }
+trap cleanup EXIT
+
+start_portforward() {
+ [[ -n "$PF_PID" ]] && kill "$PF_PID" 2>/dev/null
+ oc port-forward "$POD" "${LIBERTY_PORT}:${LIBERTY_PORT}" "${KEYCLOAK_PORT}:${KEYCLOAK_PORT}" \
+ 2>/dev/null &
+ PF_PID=$!
+}
+
+# ── 1. Resolve pod name ──────────────────────────────────────────────────────
+POD=$(oc get pod -l "$LABEL" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
+if [[ -z "$POD" ]]; then
+ echo "ERROR: no pod found with label $LABEL" >&2
+ exit 1
+fi
+echo "Using pod: $POD"
+
+# ── 2. Wait for Keycloak container to be ready ───────────────────────────────
+echo "Waiting for Keycloak container to be ready..."
+oc wait pod "$POD" --for=condition=Ready --timeout=300s 2>/dev/null || true
+
+# ── 3. Port-forward and wait for Keycloak OIDC endpoint ─────────────────────
+echo "Waiting for Keycloak realm..."
+start_portforward
+for i in $(seq 1 90); do
+ # Restart port-forward if it died
+ if ! kill -0 "$PF_PID" 2>/dev/null; then
+ sleep 3
+ start_portforward
+ fi
+ curl -sf "http://localhost:${KEYCLOAK_PORT}/realms/liberty/.well-known/openid-configuration" \
+ -o /dev/null 2>/dev/null && break
+ sleep 3
+done
+
+# ── 4. Obtain JWT from Keycloak ──────────────────────────────────────────────
+echo "Obtaining token..."
+TOKEN=$(curl -sf -X POST \
+ "http://localhost:${KEYCLOAK_PORT}/realms/liberty/protocol/openid-connect/token" \
+ -d "grant_type=password&client_id=metrics-client&client_secret=metrics-secret&username=metrics&password=metrics" \
+ | jq -r .access_token)
+
+if [[ -z "$TOKEN" ]] || [[ "$TOKEN" == "null" ]]; then
+ echo "ERROR: failed to obtain token — check Keycloak logs with:" >&2
+ echo " oc logs $POD -c keycloak | grep -i error" >&2
+ exit 1
+fi
+echo "Token obtained."
+
+# ── 5. Fetch metrics ─────────────────────────────────────────────────────────
+echo ""
+echo "GET https://localhost:${LIBERTY_PORT}${METRICS_PATH}"
+echo "────────────────────────────────────────────────────"
+HTTP_CODE=$(curl -sk -o /tmp/metrics_response.txt -w "%{http_code}" \
+ -H "Authorization: Bearer $TOKEN" \
+ "https://localhost:${LIBERTY_PORT}${METRICS_PATH}")
+echo "HTTP $HTTP_CODE"
+cat /tmp/metrics_response.txt
diff --git a/dev/sso.xml b/dev/sso.xml
new file mode 100644
index 00000000..f145b6ab
--- /dev/null
+++ b/dev/sso.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..2ce345d7
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,37 @@
+services:
+
+ keycloak:
+ image: quay.io/keycloak/keycloak:25.0
+ command: start-dev --import-realm
+ environment:
+ KEYCLOAK_ADMIN: admin
+ KEYCLOAK_ADMIN_PASSWORD: admin
+ volumes:
+ - ./dev/keycloak-realm.json:/opt/keycloak/data/import/realm.json:ro
+ ports:
+ - "8080:8080"
+ healthcheck:
+ test: ["CMD-SHELL", "curl -sf http://localhost:8080/realms/liberty/.well-known/openid-configuration || exit 1"]
+ interval: 10s
+ timeout: 5s
+ retries: 12
+
+ app:
+ build: .
+ volumes:
+ # Mount the dev SSO include file into the Liberty config includes directory
+ - ./dev/sso.xml:/config/configDropins/overrides/sso.xml:ro
+ ports:
+ - "9443:9443"
+ depends_on:
+ keycloak:
+ condition: service_healthy
+
+# To get a token and hit /metrics:
+#
+# TOKEN=$(curl -s -X POST \
+# http://localhost:8080/realms/liberty/protocol/openid-connect/token \
+# -d "grant_type=password&client_id=metrics-client&client_secret=metrics-secret&username=metrics&password=metrics" \
+# | jq -r .access_token)
+#
+# curl -k -H "Authorization: Bearer $TOKEN" https://localhost:9443/metrics
diff --git a/src/main/liberty/config/server.xml b/src/main/liberty/config/server.xml
index 7f92a611..21dec806 100644
--- a/src/main/liberty/config/server.xml
+++ b/src/main/liberty/config/server.xml
@@ -6,15 +6,21 @@
mpMetrics-5.1
mpHealth-4.0
mpConfig-3.1
+
+ mpJwt-2.1
-
+
+
+
+
+
+
-
-
+