feat(operator): operator-managed Keycloak client registration#247
feat(operator): operator-managed Keycloak client registration#247akram wants to merge 4 commits intokagenti:mainfrom
Conversation
d8863ed to
3d8c0b5
Compare
cwiklik
left a comment
There was a problem hiding this comment.
Review Summary
Well-designed Keycloak client registration controller with sound architecture: Secret-before-annotation ordering, owner references for GC, feature-gated, and uncached API reads for namespace config. Thorough documentation.
Key concerns: (1) indentation bug in the StatefulSet path, (2) Made-with: Cursor trailer violates commit conventions, (3) defaulting the feature to true is aggressive for a new controller. The Keycloak client is create-only (no update path).
Areas reviewed: Go, Helm/K8s RBAC, Makefile, Docs
Commits: 3 commits, all signed-off: yes
CI status: DCO passing
| // DefaultHTTPClient returns an HTTP client suitable for Keycloak admin calls. | ||
| func DefaultHTTPClient() *http.Client { | ||
| return &http.Client{Timeout: 60 * time.Second} | ||
| } |
There was a problem hiding this comment.
suggestion: DefaultHTTPClient() uses http.DefaultTransport which verifies TLS (good), but there's no way to inject a custom CA bundle for in-cluster Keycloak with a private CA. Consider accepting a transport option or *tls.Config for production clusters with internal CAs.
| } | ||
| _, clientSecret, err := kc.RegisterOrFetchClientWithToken(ctx, token, keycloak.ClientRegistrationParams{ | ||
| Realm: ab.KeycloakRealm, | ||
| ClientID: clientID, |
There was a problem hiding this comment.
suggestion: The admin token is obtained fresh via PasswordGrantToken on every reconcile. For workloads reconciling frequently, this generates one Keycloak token request per reconcile. Consider caching the token (with expiry check) to reduce Keycloak load.
| continue | ||
| } | ||
| if v, ok := m["globalEnabled"].(bool); ok { | ||
| globalOn = v |
There was a problem hiding this comment.
nit: readClusterFeatureGates iterates cm.Data and returns after parsing the first non-empty YAML document. If the ConfigMap has multiple data keys, only the first is considered. A brief comment clarifying this assumption would help readability.
| verbs: | ||
| - create | ||
| - get | ||
| - list |
There was a problem hiding this comment.
suggestion: This grants cluster-wide secrets read access (get/list/watch). The controller only needs secrets in agent namespaces. Consider whether a more restrictive RBAC binding is warranted (ClusterRole + per-namespace RoleBindings), or document why cluster-wide is needed.
| Name: secretName, | ||
| Namespace: owner.GetNamespace(), | ||
| Labels: map[string]string{ | ||
| LabelManagedBy: LabelManagedByValue, |
There was a problem hiding this comment.
👍 Good use of controllerutil.CreateOrUpdate with SetControllerReference — ensures GC when the parent Deployment/StatefulSet is deleted. The Secret-before-annotation ordering is also well done.
kagenti-operator/internal/controller/clientregistration_controller_test.go
Show resolved
Hide resolved
Introduce ClientRegistrationReconciler for Deployments and StatefulSets that use kagenti.io/client-registration-inject=false: register the workload as an OAuth client in Keycloak, create credentials secrets, and annotate the pod template so the webhook can mount them without injecting the registration sidecar. Add a small Keycloak admin API client and RBAC; gate the controller behind --enable-operator-client-registration. Signed-off-by: Akram <akram.benaissi@gmail.com>
Describe motivation, webhook/operator contract, requirements, rollout and rollback, and pointers to source files. Signed-off-by: Akram <akram.benaissi@gmail.com>
- Use kagenti.io/keycloak-client-credentials-secret-name and kagenti-keycloak-client-credentials-<uniq-id> Secrets - Rename inject/annotation helpers for Keycloak client credentials; update docs Assisted-by: Cursor Signed-off-by: Akram <akram.benaissi@gmail.com>
- Cache admin tokens via CachedAdminTokenProvider; wire from main when client registration is enabled. - Default --enable-operator-client-registration to false (opt-in). - Reconcile Keycloak client settings: GET client, PUT when desired state drifts from Keycloak. - Add HTTPClientConfig / NewHTTPClient for TLS and custom transport (e.g. private CA). - Document feature-gate ConfigMap iteration; fix StatefulSet branch indentation. - Add integration-style reconciler tests and expand admin tests (drift, HTTP client). Addresses review comments. Assisted-by: Cursor Signed-off-by: Akram <akram.benaissi@gmail.com>
3d8c0b5 to
7d9d86f
Compare
Alan-Cha
left a comment
There was a problem hiding this comment.
Review Summary
This PR adds operator-managed Keycloak client registration as an alternative to sidecar-based registration. The implementation is well-designed with proper controller patterns, comprehensive tests (including envtest-style reconciler tests and mock Keycloak server), token caching, drift reconciliation, and thorough documentation.
The latest commit (7d9d86f) successfully addressed most of the previous review feedback: feature flag now defaults to false (opt-in), token caching is implemented, TLS config support added, indentation bug fixed, and test coverage expanded significantly.
One must-fix issue remains: Documentation inconsistency about the feature flag default value.
Areas reviewed: Go (controller, Keycloak admin client, tests), Makefile, RBAC, Documentation, Commit conventions
Commits: 4 commits, all DCO signed ✅
CI status: DCO passing ✅
Must-Fix Issues
Documentation default value mismatch (docs/operator-managed-client-registration.md, line ~77 in new file):
Line states the default is true, but cmd/main.go:112 sets the flag default to false. The docs should match the code:
-| Operator | `--enable-operator-client-registration` (default **true**) | Master switch for the ClientRegistration controller. |
+| Operator | `--enable-operator-client-registration` (default **false**) | Master switch for the ClientRegistration controller. |Praise
Excellent reconciler patterns (clientregistration_controller.go):
- ✅
CreateOrUpdatewith owner references ensures proper GC - ✅ Secret-before-annotation ordering prevents pods from referencing missing Secrets
- ✅ Clean reconciler structure following controller-runtime best practices
Well-implemented drift reconciliation (internal/keycloak/admin.go):
- ✅ GET client, compare managed fields, merge desired state, PUT only when drifted
- ✅ Properly handles Keycloak's JSON shape quirks (string arrays in attributes)
Strong test coverage (clientregistration_controller_test.go):
- ✅ Reconciler integration tests with fake clients and mock Keycloak server
- ✅ Covers feature gates disabled, missing dependencies (requeue), and happy path
- ✅ Addresses previous review's testing concern
Suggestions
Token cache tuning (cmd/main.go): Token cache is instantiated with a hardcoded tokenCacheSkew. Consider exposing this as a flag if operators need to tune it for their Keycloak instance characteristics.
Commit 3 convention: Subject lacks emoji prefix: operator: Keycloak client credentials Secret naming and annotation. Should be feat(operator): or refactor(operator): per Kagenti conventions. Not blocking since commits can be squashed on merge.
Once the documentation is corrected, this PR is ready to merge. The implementation quality is high, test coverage is comprehensive, and all previous review concerns have been addressed.
Summary
Registers Keycloak OAuth clients for agent/tool workloads that set
kagenti.io/client-registration-inject: "false", creates a Secret withclient-id.txt/client-secret.txt, and annotates the pod template so the kagenti-webhook mounts credentials intoshared-data.requires:
requires: kagenti/kagenti-extensions#262
Changes
ClientRegistrationReconcilerfor Deployment/StatefulSet; Keycloak admin client (internal/keycloak).kagenti-keycloak-client-credentials-<hash>; annotationkagenti.io/keycloak-client-credentials-secret-name.--enable-operator-client-registration; APIReader for authbridge-config / keycloak-admin-secret; RBAC for Secrets.docs/operator-managed-client-registration.md.Related
Testing
fixes kagenti/kagenti#397