Skip to content

Add startup entrypoint, multi-stage build, and aligned CI/CD - #4

Merged
johardi merged 12 commits into
mainfrom
feature/startup-entrypoint
Apr 17, 2026
Merged

Add startup entrypoint, multi-stage build, and aligned CI/CD#4
johardi merged 12 commits into
mainfrom
feature/startup-entrypoint

Conversation

@matthewhorridge

@matthewhorridge matthewhorridge commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR eliminates the manual Keycloak setup steps that operators currently have to perform after deploying WebProtege. It also modernises the build and release process to match the patterns used by other webprotege services.

Before this PR

After starting Keycloak, operators had to manually:

  1. Import the realm via kcadm.sh create realms (~1 step)
  2. Delete and recreate the username protocol mapper across 4 sub-steps because Keycloak's realm import silently drops the mapper config (keycloak#36065, keycloak#16289)
  3. Verify the mapper was correct (~1 step)

Self-hosted deployments also had to manually edit the realm JSON or run kcadm commands to update redirect URIs, web origins, and the frontend URL to match their hostname.

Building the Docker image required Java and Maven installed locally to compile the SPI plugin before running docker build.

After this PR

Keycloak starts, imports the realm, fixes the mapper, and configures all URIs automatically. The operator just sets SERVER_HOST and runs docker compose up.

Building the image requires only Docker — docker build . handles everything.

What changed

Entrypoint script (entrypoint.sh)

A bash wrapper around kc.sh that runs after Keycloak starts:

  1. Realm import — imports the webprotege realm via kcadm if it doesn't already exist. Keycloak does not auto-import from /opt/keycloak/import/; the entrypoint handles this explicitly.
  2. Protocol mapper fix — checks whether the username mapper in the profile client scope correctly maps webprotege_username to preferred_username. If not, deletes and recreates it. This works around a Keycloak bug where the import drops the mapper's config dict.
  3. Hostname-based URI patching — when SERVER_HOST is set, updates the webprotege client's baseUrl, redirectUris, webOrigins, and the realm's frontendUrl to match. This makes the image portable across local dev, staging, and production.

All operations are idempotent — on subsequent restarts, the script detects the correct state and skips modifications. SIGTERM/SIGINT are forwarded to the Keycloak process for graceful shutdown.

Multi-stage Dockerfile

  • SPI build stage — compiles the authenticator plugin inside Docker using maven:3.9-eclipse-temurin-17. No local Java or Maven needed.
  • jq download stage — fetches a static jq binary from GitHub releases (architecture-aware via TARGETARCH). Used by the entrypoint to parse kcadm JSON output.
  • Final stage — based on keycloak/keycloak:26.1 as before, with the entrypoint set as the image's ENTRYPOINT.

Integration test (test-entrypoint.sh)

A self-contained test script that verifies the entrypoint end-to-end:

  • Starts the container with a test SERVER_HOST
  • Asserts: realm imported, mapper config correct, client URIs rewritten, frontend URL updated
  • Restarts the container with a persistent volume
  • Asserts: realm import skipped, mapper fix skipped, state unchanged

15 assertions total, runs in ~60 seconds. No host port needed (all checks run inside the container via docker exec).

CI/CD workflows

Aligned with the pattern used by other webprotege services:

  • ci.yaml — runs on feature branches and PRs. Builds the image and runs the integration test.
  • release.yaml — triggers on merge to main. Reads the version from pom.xml, strips -SNAPSHOT, releases that version, then bumps patch and adds -SNAPSHOT back for the next development cycle. Includes the integration test as a gate before pushing to Docker Hub. Notifies the deploy repo after a successful release.
  • notify-deploy-project.yaml — reusable workflow that triggers update-compose.yml in webprotege-deploy to update the image version in docker-compose.yml.

Versioning

Follows the standard Maven -SNAPSHOT convention:

  • pom.xml contains the next intended version with -SNAPSHOT (e.g. 2.0.0-SNAPSHOT)
  • On merge to main, the workflow strips -SNAPSHOT and releases that version (2.0.0)
  • After release, the workflow bumps patch and sets 2.0.1-SNAPSHOT
  • Major/minor bumps: set the pom to X.Y.0-SNAPSHOT on your branch before merging

Branch protection

Added a "Protect Main" ruleset matching webprotege-backend-service: no deletion, no force push, PRs required, bot bypass for release commits.

Test plan

  • Docker image builds successfully (docker build .)
  • Integration test passes locally (15/15 assertions)
  • First boot: realm imported, mapper correct, URIs patched
  • Restart with volume: import skipped, mapper skipped, state preserved
  • No host port required (no port conflicts)
  • CI workflow runs on this PR

matthewhorridge and others added 4 commits April 14, 2026 11:04
…ostnames

Keycloak's realm import silently drops the config for the 'username'
protocol mapper in the 'profile' client scope, reverting it to defaults.
The realm JSON also has redirect URIs and web origins hardcoded to a
specific hostname, making the image non-portable across environments.

This adds an entrypoint script that wraps kc.sh, waits for the realm
import to complete, then idempotently patches the mapper config and
(when SERVER_HOST is set) rewrites client URIs to match the deployment
hostname.

See: keycloak/keycloak#36065
See: keycloak/keycloak#16289

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the tag-triggered release workflow with the standard
auto-bump-on-main pattern used by other webprotege services.
Merge to main now auto-increments the patch version, builds and
tests the Docker image, pushes to Docker Hub, creates a GitHub
release, and notifies the deploy repo to update docker-compose.yml.

Rename test.yaml to ci.yaml so it runs on feature branches and
PRs only, matching the convention in other repos.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The release workflow now follows the standard Maven convention:
pom contains the next version with -SNAPSHOT suffix, the workflow
strips it for the release, then bumps patch and adds -SNAPSHOT
back for the next development cycle.

Major/minor version changes are controlled by setting the pom
version on the branch (e.g. 3.0.0-SNAPSHOT for a major bump).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
matthewhorridge and others added 8 commits April 15, 2026 09:01
Newly registered Keycloak users do not have the webprotege_username
user attribute, which the username protocol mapper maps to the
preferred_username JWT claim. Without it, the claim is absent from
tokens and the API gateway treats the user as null, causing 400 errors.

This adds a Keycloak EventListenerProvider SPI that fires on REGISTER
events and sets webprotege_username to the Keycloak username for any
newly registered user who does not already have the attribute. Migrated
users are unaffected since their attribute is pre-populated.

The listener is registered in the realm's eventsListeners array so
Keycloak invokes it when users register.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The webprotege client previously defined six client roles (user, admin,
SystemAdmin, ICAT_APPLICATION, ProjectCreator, ProjectManager). Only
SystemAdmin was actually used — the authorization service's JwtRolesExtractor
reads the resource_access.webprotege.roles claim and maps values to
BuiltInRole entries. The other five roles were either not in BuiltInRole
at all (user, admin, ICAT_APPLICATION) or were project-level roles that
should not be granted globally via Keycloak (ProjectManager) or had an
alternative path through Application Settings (ProjectCreator).

Also removed unused realm-level roles (admin, SYSTEM_ADMIN) and updated
the default-roles-webprotege composite to drop references to them. The
realm-level SYSTEM_ADMIN was particularly misleading — the code only
reads client-level roles, so this realm role was a silent no-op despite
being granted to every user via the default composite.

Removed the ICAT_APPLICATION assignment from the affected service-account
user. The separate Icatx_application client (which also defines its own
ICAT_APPLICATION role) is untouched.

After cleanup, SystemAdmin is the only application-level role exposed via
Keycloak. All other authorization decisions flow through WebProtege's own
RoleAssignment mechanism or Application Settings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Icatx_application client and its service-account user were part of the
ICAT-X customization of WebProtege, which lives on a separate branch
parallel to main. Keeping it in the main realm bakes ICAT-X-specific
authorization objects into every vanilla WebProtege deployment.

Removed:
- The Icatx_application client definition
- The roles.client.Icatx_application entry (uma_protection, ICAT_APPLICATION)
- The service-account-icatx_application user

The ICAT-X branch can re-add these via its own fork of webprotege.json
or via post-import kcadm commands in its own entrypoint customization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Explain what the role grants, when to use it (first admin bootstrap),
and the underlying mechanism (JWT resource_access.webprotege.roles
mapped to BuiltInRole capabilities). The description is visible in
the Keycloak admin console on the role detail page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous description (1008 chars) exceeded Keycloak's 255-char
VARCHAR limit on the role description column, causing a database error
when imported or saved via the admin API. Condensed to 248 chars while
keeping the essentials: what it grants, when to use it, and how it
maps to the code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The machine-client Keycloak client is not referenced by any vanilla
WebProtege service.

If it is used by a downstream customization (e.g. ICAT-X), that fork
can re-add it in its own realm JSON or via post-import kcadm commands.

Removed:
- The machine-client client definition and its protocol mappers
- The roles.client.machine-client entry
- The service-account-machine-client user

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a description to each client explaining its purpose: what backs
it, who talks to it, and (where relevant) how it integrates with
WebProtege services.

Covers both the WebProtege-specific clients (webprotege, user-management)
and the Keycloak built-in clients (account, account-console, admin-cli,
broker, realm-management, security-admin-console). Someone reading the
realm for the first time now has context on every entry without having
to infer from URL paths or client settings.

All descriptions are under 255 chars to fit the Keycloak DB column
limit on the CLIENT description column.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a 'Roles' section explaining that SystemAdmin is the only
application-level role exposed via Keycloak and linking to the
deploy repo README for the step-by-step bootstrap procedure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@johardi
johardi merged commit 04e9b87 into main Apr 17, 2026
2 checks passed
@johardi
johardi deleted the feature/startup-entrypoint branch April 17, 2026 18:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants