Skip to content

Commit 04e9b87

Browse files
authored
Merge pull request #4 from protegeproject/feature/startup-entrypoint
Add startup entrypoint, multi-stage build, and aligned CI/CD
2 parents 6973cfa + 2bc0af2 commit 04e9b87

13 files changed

Lines changed: 1201 additions & 365 deletions

.github/workflows/ci.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
on:
2+
push:
3+
branches-ignore:
4+
- main
5+
pull_request:
6+
7+
jobs:
8+
build:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- uses: actions/checkout@v4
13+
- name: Build image
14+
run: docker build -t protegeproject/webprotege-keycloak:test .
15+
- name: Run entrypoint integration test
16+
run: ./test-entrypoint.sh
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: Notify WebProtege Deploy
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
service:
7+
required: true
8+
type: string
9+
version:
10+
required: true
11+
type: string
12+
branch_var:
13+
required: true
14+
type: string
15+
secrets:
16+
PROTEGE_PROJECT_CLIENT_ID:
17+
required: true
18+
PROTEGE_PROJECT_CLIENT_SECRET:
19+
required: true
20+
21+
jobs:
22+
notify:
23+
runs-on: ubuntu-latest
24+
25+
steps:
26+
- name: Generate GitHub App Token
27+
id: app-token
28+
uses: actions/create-github-app-token@v1
29+
with:
30+
app-id: ${{ secrets.PROTEGE_PROJECT_CLIENT_ID }}
31+
private-key: ${{ secrets.PROTEGE_PROJECT_CLIENT_SECRET }}
32+
owner: protegeproject
33+
repositories: webprotege-deploy
34+
35+
- name: Trigger workflow_call in webprotege-deploy
36+
run: |
37+
SERVICE="${{ inputs.service }}"
38+
VERSION="${{ inputs.version }}"
39+
BRANCH="${{ inputs.branch_var }}"
40+
echo "Triggering webprotege-deploy with service=$SERVICE and version=$VERSION"
41+
42+
response=$(curl -s -w "%{http_code}" -X POST \
43+
-H "Authorization: token ${{ steps.app-token.outputs.token }}" \
44+
-H "Accept: application/vnd.github+json" \
45+
https://api.github.com/repos/protegeproject/webprotege-deploy/actions/workflows/update-compose.yml/dispatches \
46+
-d '{
47+
"ref": "'"$BRANCH"'",
48+
"inputs": {
49+
"service": "'"$SERVICE"'",
50+
"version": "'"$VERSION"'",
51+
"branch": "'"$BRANCH"'"
52+
}
53+
}')
54+
55+
http_code=$(echo "$response" | tail -n1)
56+
body=$(echo "$response" | sed '$d')
57+
58+
echo "HTTP Status Code: $http_code"
59+
echo "Response Body: $body"
60+
61+
if [[ "$http_code" -lt 200 || "$http_code" -ge 300 ]]; then
62+
echo "Error: API call failed with status code $http_code"
63+
exit 1
64+
else
65+
echo "API call successful, status code: $http_code"
66+
fi

.github/workflows/release.yaml

Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,120 @@ name: Release
22

33
on:
44
push:
5-
tags:
6-
- 'v*.*.*'
5+
branches:
6+
- main
77

88
jobs:
99
build:
1010
runs-on: ubuntu-latest
11+
if: ${{ github.actor != 'protegeproject-bot[bot]' }}
12+
outputs:
13+
version: ${{ steps.release-outputs.outputs.version }}
14+
service: ${{ steps.release-outputs.outputs.service }}
15+
1116
steps:
1217
- name: Login to Docker Hub
1318
uses: docker/login-action@v2
1419
with:
1520
username: ${{secrets.DOCKER_USERNAME}}
1621
password: ${{secrets.DOCKER_PASSWORD}}
22+
- uses: actions/create-github-app-token@v1
23+
id: app-token
24+
with:
25+
app-id: ${{ vars.PROTEGEPROJECT_BOT_APP_ID }}
26+
private-key: ${{ secrets.PROTEGEPROJECT_BOT_APP_PRIVATE_KEY }}
1727
- uses: actions/checkout@v4
18-
- name: Set up Java
28+
with:
29+
token: ${{ steps.app-token.outputs.token }}
30+
ref: ${{ github.head_ref }}
31+
- name: Set up Maven Central Repository
1932
uses: actions/setup-java@v3
2033
with:
2134
java-version: '17'
2235
distribution: 'adopt'
23-
- name: Extract version from tag
24-
id: get-version
36+
server-id: docker.io
37+
server-username: DOCKER_USERNAME
38+
server-password: DOCKER_PASSWORD
39+
40+
# Read the version from pom.xml and strip the -SNAPSHOT suffix to
41+
# produce the release version. For example, 2.0.0-SNAPSHOT becomes
42+
# 2.0.0. If there is no -SNAPSHOT suffix the version is used as-is.
43+
- name: Determine release version
44+
id: release-version
45+
run: |
46+
current_version=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
47+
echo "Current pom version: $current_version"
48+
release_version=$(echo "$current_version" | sed -E 's/-SNAPSHOT$//')
49+
echo "Release version: $release_version"
50+
echo "release_version=$release_version" >> $GITHUB_OUTPUT
51+
52+
# Set pom.xml to the release version (without -SNAPSHOT), commit,
53+
# and tag. This is the commit that the Docker image is built from.
54+
- name: Set release version in pom.xml
2555
run: |
26-
VERSION=${GITHUB_REF_NAME#v}
27-
echo "version=$VERSION" >> $GITHUB_OUTPUT
28-
echo "Version: $VERSION"
29-
- name: Set version in pom.xml files
56+
find . -name 'pom.xml' -exec mvn versions:set \
57+
-DnewVersion=${{ steps.release-version.outputs.release_version }} \
58+
-DgenerateBackupPoms=false -f {} \;
59+
- name: Commit and tag release
3060
run: |
31-
VERSION=${{ steps.get-version.outputs.version }}
32-
find . -name 'pom.xml' -exec mvn versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false -f {} \;
33-
- name: Build SPI plugin
34-
run: mvn --batch-mode -pl spi clean package
35-
- name: Build and push Docker image
36-
run: mvn --batch-mode package install
61+
git config --global user.name "github-actions[bot]"
62+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
63+
git add .
64+
# If the pom already had the release version (no -SNAPSHOT suffix),
65+
# there is nothing to commit — just tag the current HEAD.
66+
git diff --cached --quiet || \
67+
git commit -m "Release ${{ steps.release-version.outputs.release_version }}"
68+
git tag ${{ steps.release-version.outputs.release_version }}
69+
git push origin HEAD:${GITHUB_REF##*/}
70+
git push origin ${{ steps.release-version.outputs.release_version }}
71+
72+
- name: Build Docker image
73+
run: mvn --batch-mode package
74+
- name: Run entrypoint integration test
75+
run: ./test-entrypoint.sh protegeproject/webprotege-keycloak:${{ steps.release-version.outputs.release_version }}
76+
- name: Push Docker image
77+
run: docker push protegeproject/webprotege-keycloak:${{ steps.release-version.outputs.release_version }}
3778
- name: Release
3879
uses: softprops/action-gh-release@v1
3980
env:
4081
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4182
with:
42-
tag_name: ${{ github.ref_name }}
83+
tag_name: ${{ steps.release-version.outputs.release_version }}
4384
generate_release_notes: true
4485

86+
# After the release, bump the patch version and add -SNAPSHOT to
87+
# prepare the pom for the next development cycle. For example,
88+
# after releasing 2.0.0 the pom is set to 2.0.1-SNAPSHOT.
89+
- name: Prepare next development version
90+
run: |
91+
released=${{ steps.release-version.outputs.release_version }}
92+
IFS='.' read -r -a parts <<< "$released"
93+
parts[2]=$((parts[2] + 1))
94+
next_snapshot="${parts[0]}.${parts[1]}.${parts[2]}-SNAPSHOT"
95+
echo "Next development version: $next_snapshot"
96+
find . -name 'pom.xml' -exec mvn versions:set \
97+
-DnewVersion=$next_snapshot \
98+
-DgenerateBackupPoms=false -f {} \;
99+
git add .
100+
git commit -m "Prepare next development version ($next_snapshot)"
101+
git push origin HEAD:${GITHUB_REF##*/}
102+
103+
- name: Set outputs
104+
id: release-outputs
105+
run: |
106+
echo "version=${{ steps.release-version.outputs.release_version }}" >> $GITHUB_OUTPUT
107+
echo "service=webprotege-keycloak" >> $GITHUB_OUTPUT
108+
109+
notify-bump:
110+
needs: build
111+
uses: ./.github/workflows/notify-deploy-project.yaml
112+
with:
113+
version: ${{ needs.build.outputs.version }}
114+
service: ${{ needs.build.outputs.service }}
115+
branch_var: ${{vars.BUMP_WEBPROTEGE_BRANCH}}
116+
secrets:
117+
PROTEGE_PROJECT_CLIENT_ID: ${{ secrets.PROTEGE_PROJECT_CLIENT_ID }}
118+
PROTEGE_PROJECT_CLIENT_SECRET: ${{ secrets.PROTEGE_PROJECT_CLIENT_SECRET }}
45119

46120
env:
47121
DOCKER_USERNAME: ${{secrets.DOCKER_USERNAME}}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Maven build output
22
spi/target/
3+
*.versionsBackup
34

45
# JVM
56
hsperfdata_keycloak/

Dockerfile

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
1+
FROM alpine:3 AS tools
2+
ARG TARGETARCH
3+
RUN wget -O /usr/local/bin/jq \
4+
"https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-${TARGETARCH}" \
5+
&& chmod +x /usr/local/bin/jq
6+
7+
FROM maven:3.9-eclipse-temurin-17 AS spi-builder
8+
WORKDIR /build
9+
COPY spi/pom.xml .
10+
RUN mvn dependency:go-offline -B
11+
COPY spi/src ./src
12+
RUN mvn package -B -DskipTests
13+
114
FROM keycloak/keycloak:26.1
15+
COPY --from=tools /usr/local/bin/jq /usr/bin/jq
216
COPY ./webprotege /opt/keycloak/themes/webprotege
3-
COPY ./spi/target/webprotege-credential-check-authenticator-*.jar /opt/keycloak/providers/
17+
COPY --from=spi-builder /build/target/webprotege-credential-check-authenticator-*.jar /opt/keycloak/providers/
418
COPY ./webprotege.json /opt/keycloak/import/webprotege.json
19+
COPY --chmod=755 ./entrypoint.sh /opt/keycloak/bin/entrypoint.sh
20+
521
RUN /opt/keycloak/bin/kc.sh build
22+
23+
ENTRYPOINT ["/opt/keycloak/bin/entrypoint.sh"]

README.md

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,39 +8,89 @@ Keycloak configuration for WebProtege. This repository contains:
88

99
## Prerequisites
1010

11-
- Java 17+
12-
- Maven 3.8+
1311
- Docker
1412

15-
## Building the Plugin
13+
## Docker Build
14+
15+
The `Dockerfile` uses a multi-stage build to compile the authenticator plugin,
16+
download tools, and package the theme, realm configuration, and startup
17+
entrypoint into a custom Keycloak image. No local Java or Maven installation
18+
is required.
1619

17-
The authenticator plugin must be built before deploying Keycloak:
20+
To build locally:
1821

1922
```bash
20-
cd spi
21-
mvn clean package
23+
docker build -t protegeproject/webprotege-keycloak:1.2.0 .
2224
```
2325

24-
This produces `spi/target/webprotege-credential-check-authenticator-1.0.0.jar`.
26+
## Startup Entrypoint
2527

26-
## Docker Build
28+
The image includes a custom entrypoint script (`entrypoint.sh`) that wraps the
29+
standard Keycloak startup. It starts Keycloak normally, waits for the realm
30+
import to complete, then applies two configuration patches that cannot be
31+
achieved through the realm import alone.
2732

28-
The `Dockerfile` packages the theme, plugin, and realm configuration into a custom Keycloak image:
33+
### 1. Protocol Mapper Fix
2934

30-
```dockerfile
31-
FROM keycloak/keycloak:26.1
32-
COPY ./webprotege /opt/keycloak/themes/webprotege
33-
COPY ./spi/target/webprotege-credential-check-authenticator-1.0.0.jar /opt/keycloak/providers/
34-
COPY ./webprotege.json /opt/keycloak/import/webprotege.json
35-
RUN /opt/keycloak/bin/kc.sh build
36-
```
35+
Keycloak's realm import mechanism has a known limitation: it silently drops the
36+
`config` dictionary for certain protocol mapper types. The realm JSON defines a
37+
`username` mapper in the `profile` client scope that maps the custom user
38+
attribute `webprotege_username` to the `preferred_username` JWT claim. After
39+
import, Keycloak reverts this mapper to its built-in default, which maps the
40+
Keycloak username field instead.
3741

38-
To build locally:
42+
This matters because WebProtege uses email addresses as Keycloak usernames, but
43+
internal application lookups rely on the original MongoDB user ID stored in the
44+
`webprotege_username` attribute. Without the fix, the backend receives email
45+
addresses where it expects user IDs, breaking user resolution.
3946

40-
```bash
41-
cd spi && mvn clean package && cd ..
42-
docker build -t protegeproject/webprotege-keycloak:1.0.0 .
43-
```
47+
The entrypoint detects this condition on each startup and, if the mapper is in
48+
the wrong state, deletes it and recreates it with the correct configuration.
49+
On subsequent boots where the mapper is already correct, the fix is skipped.
50+
51+
### 2. Hostname-Based Client URI Patching
52+
53+
The realm JSON ships with a default hostname baked into the `webprotege`
54+
client's redirect URIs, web origins, and base URL. Self-hosted deployments use
55+
different hostnames. When the `SERVER_HOST` environment variable is set, the
56+
entrypoint updates these values so that:
57+
58+
- Keycloak accepts OAuth redirects back to the correct host
59+
- CORS headers include the correct origin
60+
- The Keycloak login and account pages link back to the correct application URL
61+
- The realm's OpenID Connect discovery document advertises the correct issuer
62+
63+
This makes the image portable — the same build works for local development,
64+
staging, and production by setting `SERVER_HOST` in the deployment environment.
65+
66+
### Environment Variables
67+
68+
| Variable | Required | Default | Purpose |
69+
|---|---|---|---|
70+
| `KEYCLOAK_ADMIN` | Yes | `admin` | Admin username for kcadm authentication |
71+
| `KEYCLOAK_ADMIN_PASSWORD` | Yes | `password` | Admin password for kcadm authentication |
72+
| `KC_HTTP_RELATIVE_PATH` | Yes | *(none)* | Keycloak's HTTP relative path (e.g. `/keycloak`) |
73+
| `SERVER_HOST` | No | *(none)* | Public hostname; when set, client URIs and the realm frontend URL are updated to match |
74+
75+
## Roles
76+
77+
The realm defines a single application-level role on the `webprotege`
78+
client: **`SystemAdmin`**. This is the bootstrap admin role for new
79+
installs — assigning it to a user grants full administrative access to
80+
WebProtege (manage application settings, create projects, edit roles,
81+
delete accounts, etc.).
82+
83+
See the [webprotege-deploy README](../webprotege-deploy/README.md#first-admin-bootstrap)
84+
for step-by-step instructions on assigning the role to the first admin.
85+
86+
All other authorization in WebProtege is managed inside the application
87+
itself via the authorization service's `RoleAssignment` collection and
88+
the Application Settings UI. Keycloak roles are used only for the
89+
bootstrap admin — this keeps identity (Keycloak) and authorization
90+
(WebProtege) cleanly separated.
91+
92+
Automating the initial admin assignment on fresh installs is tracked in
93+
[webprotege-authorization-service#36](https://github.com/protegeproject/webprotege-authorization-service/issues/36).
4494

4595
## Deployment
4696

0 commit comments

Comments
 (0)