Skip to content

Commit cb04b0a

Browse files
committed
feat(keycloak): replace shell-based realm setup with keycloak-config-cli
Phase 1a of the declarative Keycloak configuration design proposed in #154. Replaces the imperative shell script in realm-setup-job.yaml with a declarative YAML applied by keycloak-config-cli (kcc) at PostSync. Changes: - Add `realm-config-cm.yaml`: ConfigMap carrying the kcc-format YAML for the nebari realm. Parity with the previous shell setup, including the `groups` client scope + group-membership mapper (load-bearing for operator-managed app authorization), the `argocd` OIDC client (PR #234), the `argocd-admins`/`argocd-viewers` groups, and the admin user's `argocd-admins` membership. - Rewrite `realm-setup-job.yaml`: pulls `quay.io/adorsys/keycloak-config-cli:6.5.0-26.5.5`, mounts the ConfigMap, and applies it against Keycloak. Secrets are mounted from the existing `nebari-realm-admin-credentials` and `argocd-oidc-client-secret` Secrets and substituted into the realm YAML by kcc at apply time via `$(env:VAR)` placeholders. Operator coexistence: - `IMPORT_REMOTESTATE_ENABLED=true` (kcc default since v6.x): kcc tracks resources it created in a Keycloak realm attribute. Re-runs only reconcile what kcc itself owns; operator-managed OAuth2 clients created at runtime via NebariApplication CRDs are invisible to kcc's delete logic. - `IMPORT_MANAGED_CLIENT=no-delete`: belt-and-braces. Even if a future input change accidentally removes the argocd client from `clients:`, kcc will not delete unlisted clients. The strict "omit `clients:` entirely" pattern from my #154 review was too restrictive: NIC owns the `argocd` client at bootstrap, so it has to be in the input. The two-layer protection above is the practical shape of the coexistence story. Phase 1a scope (per #154 review): only replaces the existing realm setup. No `keycloak:` section in `config.yaml`, no NIC-side env-var scanning, no merge engine. Defaults are static. Future phases add the user-facing schema (1b) and the user-secrets pipeline (1c). Migration: on existing deployments, kcc detects existing resources by their natural keys (realm name, role name, username, scope name) and updates them in place, adding them to remote state. First run on an already-configured cluster is a no-op for existing managed values; the pre-generated argocd OIDC client secret (same value on both sides) is re-applied verbatim. Refs #154.
1 parent 67d5f23 commit cb04b0a

2 files changed

Lines changed: 129 additions & 119 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
apiVersion: v1
2+
kind: ConfigMap
3+
metadata:
4+
name: keycloak-realm-config
5+
namespace: keycloak
6+
labels:
7+
app.kubernetes.io/part-of: nebari-foundational
8+
app.kubernetes.io/managed-by: nebari-infrastructure-core
9+
data:
10+
# keycloak-config-cli input for the nebari realm.
11+
# Parity with the previous imperative shell setup, expressed declaratively.
12+
# `$(env:VAR)` placeholders are substituted by kcc at apply time from env
13+
# vars set on the Job (see realm-setup-job.yaml).
14+
realm.yaml: |
15+
realm: nebari
16+
enabled: true
17+
displayName: Nebari
18+
sslRequired: external
19+
registrationAllowed: false
20+
loginWithEmailAllowed: true
21+
resetPasswordAllowed: true
22+
bruteForceProtected: true
23+
24+
defaultDefaultClientScopes:
25+
- groups
26+
27+
roles:
28+
realm:
29+
- name: admin
30+
description: Administrator role
31+
- name: user
32+
description: Regular user role
33+
34+
groups:
35+
- name: argocd-admins
36+
- name: argocd-viewers
37+
38+
users:
39+
- username: admin
40+
enabled: true
41+
emailVerified: true
42+
firstName: Admin
43+
lastName: User
44+
email: admin@nebari.local
45+
realmRoles:
46+
- admin
47+
- user
48+
credentials:
49+
- type: password
50+
value: $(env:REALM_ADMIN_PASSWORD)
51+
groups:
52+
- /argocd-admins
53+
54+
clientScopes:
55+
- name: groups
56+
protocol: openid-connect
57+
attributes:
58+
include.in.token.scope: "true"
59+
display.on.consent.screen: "true"
60+
protocolMappers:
61+
- name: group-membership
62+
protocol: openid-connect
63+
protocolMapper: oidc-group-membership-mapper
64+
config:
65+
full.path: "false"
66+
introspection.token.claim: "true"
67+
userinfo.token.claim: "true"
68+
multivalued: "true"
69+
id.token.claim: "true"
70+
access.token.claim: "true"
71+
claim.name: groups
72+
73+
clients:
74+
- clientId: argocd
75+
enabled: true
76+
protocol: openid-connect
77+
publicClient: false
78+
secret: $(env:ARGOCD_CLIENT_SECRET)
79+
redirectUris:
80+
- https://argocd.{{ .Domain }}/auth/callback
81+
webOrigins:
82+
- https://argocd.{{ .Domain }}
83+
directAccessGrantsEnabled: false
84+
standardFlowEnabled: true
85+
defaultClientScopes:
86+
- groups

pkg/argocd/templates/manifests/keycloak/realm-setup-job.yaml

Lines changed: 43 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -16,137 +16,61 @@ spec:
1616
restartPolicy: OnFailure
1717
containers:
1818
- name: realm-setup
19-
image: quay.io/keycloak/keycloak:24.0
19+
# Pinned to a kcc release verified in CI against the deployed
20+
# Keycloak version. See:
21+
# https://redirect.github.com/adorsys/keycloak-config-cli/blob/v6.5.0/.github/workflows/ci.yaml
22+
image: quay.io/adorsys/keycloak-config-cli:6.5.0-26.5.5
2023
env:
21-
- name: KEYCLOAK_ADMIN_PASSWORD
24+
# Keycloak connection
25+
- name: KEYCLOAK_URL
26+
value: {{ .KeycloakServiceURL }}
27+
- name: KEYCLOAK_USER
28+
value: admin
29+
- name: KEYCLOAK_PASSWORD
2230
valueFrom:
2331
secretKeyRef:
2432
name: {{ .KeycloakAdminSecretName }}
2533
key: admin-password
34+
- name: KEYCLOAK_AVAILABILITYCHECK_ENABLED
35+
value: "true"
36+
- name: KEYCLOAK_AVAILABILITYCHECK_TIMEOUT
37+
value: 120s
38+
39+
# kcc import behavior
40+
- name: IMPORT_FILES_LOCATIONS
41+
value: /config/realm.yaml
42+
- name: IMPORT_VARSUBSTITUTION_ENABLED
43+
value: "true"
44+
- name: IMPORT_VARSUBSTITUTION_UNDEFINEDISERROR
45+
value: "true"
46+
# Default is true since kcc v6.x; explicit here for review
47+
# clarity. kcc tracks resources it created in a Keycloak realm
48+
# attribute, so re-runs only touch resources kcc itself manages.
49+
- name: IMPORT_REMOTESTATE_ENABLED
50+
value: "true"
51+
# Belt-and-braces: never delete clients on re-run, even ones
52+
# that disappear from the input. Protects operator-managed
53+
# OAuth2 clients (created at runtime via NebariApplication CRDs)
54+
# if the input config ever changes shape.
55+
- name: IMPORT_MANAGED_CLIENT
56+
value: "no-delete"
57+
58+
# Substituted into realm.yaml at kcc apply time
2659
- name: REALM_ADMIN_PASSWORD
2760
valueFrom:
2861
secretKeyRef:
2962
name: nebari-realm-admin-credentials
3063
key: password
31-
- name: KEYCLOAK_URL
32-
value: {{ .KeycloakServiceURL }}
3364
- name: ARGOCD_CLIENT_SECRET
3465
valueFrom:
3566
secretKeyRef:
3667
name: argocd-oidc-client-secret
3768
key: client-secret
38-
- name: DOMAIN
39-
value: {{ .Domain }}
40-
command:
41-
- /bin/bash
42-
- -c
43-
- |
44-
set -e
45-
KCADM="/opt/keycloak/bin/kcadm.sh"
46-
47-
echo "Waiting for Keycloak to be ready..."
48-
for i in $(seq 1 60); do
49-
if $KCADM config credentials --server "$KEYCLOAK_URL" --realm master --user admin --password "$KEYCLOAK_ADMIN_PASSWORD" 2>/dev/null; then
50-
echo "Keycloak is ready"
51-
break
52-
fi
53-
echo "Keycloak not ready, waiting... ($i/60)"
54-
sleep 5
55-
done
56-
57-
echo "Creating nebari realm..."
58-
$KCADM create realms \
59-
-s realm=nebari \
60-
-s enabled=true \
61-
-s displayName="Nebari" \
62-
-s sslRequired=external \
63-
-s registrationAllowed=false \
64-
-s loginWithEmailAllowed=true \
65-
-s resetPasswordAllowed=true \
66-
-s bruteForceProtected=true || echo "Realm may already exist"
67-
68-
echo "Creating realm roles..."
69-
$KCADM create roles -r nebari -s name=admin -s description="Administrator role" || true
70-
$KCADM create roles -r nebari -s name=user -s description="Regular user role" || true
71-
72-
echo "Creating admin user in nebari realm..."
73-
$KCADM create users -r nebari \
74-
-s username=admin \
75-
-s enabled=true \
76-
-s emailVerified=true \
77-
-s firstName=Admin \
78-
-s lastName=User \
79-
-s email=admin@nebari.local || echo "User may already exist"
80-
81-
echo "Setting admin user password..."
82-
$KCADM set-password -r nebari \
83-
--username admin \
84-
--new-password "$REALM_ADMIN_PASSWORD"
85-
86-
echo "Assigning roles to admin user..."
87-
$KCADM add-roles -r nebari --uusername admin --rolename admin || true
88-
$KCADM add-roles -r nebari --uusername admin --rolename user || true
89-
90-
echo "Configuring groups client scope with group-membership mapper..."
91-
# Look up the groups client scope ID using grep/sed (no python3 in keycloak image)
92-
GROUPS_SCOPE_ID=$($KCADM get client-scopes -r nebari --fields id,name | \
93-
grep -B1 '"name" *: *"groups"' | sed -n 's/.*"id" *: *"\([^"]*\)".*/\1/p')
94-
95-
if [ -z "$GROUPS_SCOPE_ID" ]; then
96-
echo "Creating groups client scope..."
97-
$KCADM create client-scopes -r nebari \
98-
-s name=groups \
99-
-s protocol=openid-connect \
100-
-s 'attributes={"include.in.token.scope":"true","display.on.consent.screen":"true"}' || true
101-
GROUPS_SCOPE_ID=$($KCADM get client-scopes -r nebari --fields id,name | \
102-
grep -B1 '"name" *: *"groups"' | sed -n 's/.*"id" *: *"\([^"]*\)".*/\1/p')
103-
fi
104-
105-
if [ -n "$GROUPS_SCOPE_ID" ]; then
106-
echo "Adding group-membership mapper to groups scope (id=$GROUPS_SCOPE_ID)..."
107-
$KCADM create client-scopes/$GROUPS_SCOPE_ID/protocol-mappers/models -r nebari \
108-
-s name=group-membership \
109-
-s protocol=openid-connect \
110-
-s protocolMapper=oidc-group-membership-mapper \
111-
-s 'config={"full.path":"false","introspection.token.claim":"true","userinfo.token.claim":"true","id.token.claim":"true","access.token.claim":"true","claim.name":"groups"}' || echo "Mapper may already exist"
112-
113-
echo "Ensuring groups is a realm default client scope..."
114-
$KCADM update realms/nebari/default-default-client-scopes/$GROUPS_SCOPE_ID -r nebari || true
115-
fi
116-
117-
echo "Creating ArgoCD OIDC client..."
118-
$KCADM create clients -r nebari \
119-
-s clientId=argocd \
120-
-s enabled=true \
121-
-s protocol=openid-connect \
122-
-s publicClient=false \
123-
-s "secret=$ARGOCD_CLIENT_SECRET" \
124-
-s "redirectUris=[\"https://argocd.$DOMAIN/auth/callback\"]" \
125-
-s directAccessGrantsEnabled=false \
126-
-s standardFlowEnabled=true || echo "Client may already exist"
127-
128-
# Add groups scope to argocd client as a default scope
129-
ARGOCD_CLIENT_ID=$($KCADM get clients -r nebari --fields id,clientId | \
130-
grep -B1 '"clientId" *: *"argocd"' | sed -n 's/.*"id" *: *"\([^"]*\)".*/\1/p')
131-
132-
if [ -n "$ARGOCD_CLIENT_ID" ] && [ -n "$GROUPS_SCOPE_ID" ]; then
133-
echo "Adding groups scope to argocd client..."
134-
$KCADM update clients/$ARGOCD_CLIENT_ID/default-client-scopes/$GROUPS_SCOPE_ID -r nebari || true
135-
fi
136-
137-
echo "Creating ArgoCD access groups..."
138-
$KCADM create groups -r nebari -s name=argocd-admins || echo "Group may already exist"
139-
$KCADM create groups -r nebari -s name=argocd-viewers || echo "Group may already exist"
140-
141-
echo "Adding admin user to argocd-admins group..."
142-
ADMIN_USER_ID=$($KCADM get users -r nebari -q username=admin --fields id | \
143-
sed -n 's/.*"id" *: *"\([^"]*\)".*/\1/p')
144-
ADMINS_GROUP_ID=$($KCADM get groups -r nebari --fields id,name | \
145-
grep -B1 '"name" *: *"argocd-admins"' | sed -n 's/.*"id" *: *"\([^"]*\)".*/\1/p')
146-
147-
if [ -n "$ADMIN_USER_ID" ] && [ -n "$ADMINS_GROUP_ID" ]; then
148-
$KCADM update users/$ADMIN_USER_ID/groups/$ADMINS_GROUP_ID -r nebari \
149-
-s realm=nebari -s userId=$ADMIN_USER_ID -s groupId=$ADMINS_GROUP_ID -n || true
150-
fi
151-
152-
echo "Realm setup complete!"
69+
volumeMounts:
70+
- name: realm-config
71+
mountPath: /config
72+
readOnly: true
73+
volumes:
74+
- name: realm-config
75+
configMap:
76+
name: keycloak-realm-config

0 commit comments

Comments
 (0)