Refactor Keycloak direct-access-grants into shared helper; harden eval port-forwards; add cluster/dry flags to ibac comparison - #48
Conversation
…l port-forwards; add cluster/dry flags to ibac comparison - Extract the duplicated "enable Direct Access Grants for the rossoctl client" logic from deploy-agent.sh and deploy-benchmark.sh into a new libsh/keycloak-direct-access.sh helper (enable_direct_access_grants). Resolves master-realm admin creds from env vars, the keycloak-initial-admin secret, then admin/admin defaults. - delete-all-deployments.sh now calls the helper too: its token requests use grant_type=password against the rossoctl client, which requires Direct Access Grants to be enabled first. - evaluate-benchmark.sh: detect and clean up stale kubectl port-forwards holding a local port before binding, refusing to touch non-kubectl processes; capture port-forward output to surface the real error on startup failure. - run-ibac-comparison.sh: add --kind/--openshift/--in-cluster cluster flags and a --dry run mode, forwarded to deploy-and-evaluate.sh. Signed-off-by: Yoav Katz <katz@il.ibm.com>
secret mode previously ran a client_credentials grant for the mlflow service account, but mlflow-oidc-auth authorizes reads from its own user DB (not the token's group claim), so the traces API returned 403. Switch to a password (direct-access) grant for a real MLflow user (default: admin), matching how the other deploy scripts authenticate. - analyze-run.sh: obtain the token via a password grant built from keycloak_api_url() and the mlflow-oauth-secret client id/secret; resolve the user password from KEYCLOAK_PASSWORD or the rossoctl-test-user secret; enable Direct Access Grants on the mlflow client first. - Default MLFLOW_LOCAL_PORT to 8085: 8080 is served by the kind ingress for keycloak.localtest.me (127.0.0.1), so binding there would shadow Keycloak and send the token request to MLflow. - Detect the downloader's exit 75 (token rejected) via PIPESTATUS and print an actionable hint (log into the MLflow UI once), re-echoing the original invocation. - download_mlflow_traces.py: raise MLflowAuthError on 401/403 and exit 75 so the wrapper can distinguish "no MLflow user record yet" from a generic failure. - keycloak-direct-access.sh: parameterize enable_direct_access_grants with an optional CLIENT_ID (defaults to rossoctl) so analyze-run can target the mlflow client. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Yoav Katz <katz@il.ibm.com>
Optionally tee the raw traces JSON from download_mlflow_traces.py into a timestamped file under the given directory before piping to the analyzer, so the saved copy and the analysis come from the same download. Off by default; the directory is created if needed. Also drop a stray leftover `env` debug line that dumped the full environment before the download pipeline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Yoav Katz <katz@il.ibm.com>
|
Tested this on a local Kind cluster ( The core fix is confirmedI proved the
So the password-grant prerequisite really was broken on What else I verifiedKeycloak helper — works against live Keycloak, and the flag genuinely persists (checked via the admin API, not just an HTTP 200 on the PUT). All three failure paths exit 1 with a useful diagnostic: wrong admin password, nonexistent client, unreachable Keycloak. Port-forward hardening — both branches behave correctly:
Refactor fidelity — Bug:
|
clawgenti
left a comment
There was a problem hiding this comment.
This PR introduces significant improvements to MLflow authentication, error handling, and script robustness within the A2A runner. It also adds better cluster deployment options. All checks pass. Ready for human review.
Reviewed by clawgenti using the github-pr-review skill
…chable-Keycloak diagnostic - run-ibac-comparison.sh forwarded CLUSTER_FLAG to delete-all-deployments.sh and deploy-and-evaluate.sh but not to the final analyze-run.sh comparison call, so --openshift runs targeted a Kind-style MLflow URL and failed after both benchmark runs had already completed. Forward it there too. - keycloak-direct-access.sh printed a "set KEYCLOAK_ADMIN_PASSWORD" credential hint even when the admin token request got an empty response (Keycloak unreachable). Distinguish the two: empty response reports a reachability problem against the actual KEYCLOAK_API URL. Signed-off-by: Yoav Katz <katz@il.ibm.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8a4dc86 to
ed1f8c9
Compare
…irect-access-helper
clawgenti
left a comment
There was a problem hiding this comment.
Solid refactor that eliminates meaningful code duplication and adds actionable error handling throughout. The --save-traces support, exit-75 auth-error path, and stale port-forward cleanup are all well-considered additions.
- Nit (
analyze-run.sh):--save-traces|-save-tracesaccepts a single-dash multi-character alias that isn't documented in usage and is non-standard POSIX style — consider removing the-save-tracesalternative or documenting it. - Nit (
libsh/keycloak-direct-access.sh): The Keycloak client PUT sends a partial body{"directAccessGrantsEnabled": true}; the Keycloak REST API expects the full client representation on a PUT (a PATCH-style partial update is not guaranteed). In practice it works for this flag, but it silently drops any fields not included — consider using PATCH if available, or fetching the full client config first and merging the flag in.
All checks pass. Ready for human review.
Reviewed by clawgenti using the github-pr-review skill
| --mlflow-tls) MLFLOW_TLS="true"; shift ;; | ||
| --mlflow-workspace) MLFLOW_WORKSPACE="$2"; shift 2 ;; | ||
| --auth-mode) AUTH_MODE="$2"; shift 2 ;; | ||
| --save-traces|-save-traces) SAVE_TRACES_DIR="$2"; shift 2 ;; |
There was a problem hiding this comment.
Nit: --save-traces|-save-traces — the single-dash -save-traces is a non-standard alias (POSIX single-dash flags are single-character). It's also not reflected in the usage() output or examples. Consider removing it, or if you want to keep it, document it.
| local put_code | ||
| put_code=$(curl -s -o /tmp/kc_put_response.txt -w "%{http_code}" \ | ||
| -X PUT "$KEYCLOAK_API/admin/realms/rossoctl/clients/$client_id" \ | ||
| -H "Authorization: Bearer $admin_token" \ |
There was a problem hiding this comment.
Nit: Sending {"directAccessGrantsEnabled": true} as a bare PUT body is a partial-update pattern, but the Keycloak REST API treats PUT /clients/{id} as a full-replace. For this single boolean flag it works in practice, but any fields not included in the body will be silently reset. Worth either (a) fetching the current client representation first and merging the flag, or (b) noting this footgun explicitly in the comment.
clawgenti
left a comment
There was a problem hiding this comment.
Clean refactor — the shared helper removes real duplication and the port-forward hardening is solid. One correctness issue worth addressing before merge.
Findings:
keycloak-direct-access.sh: ThePUTto/admin/realms/rossoctl/clients/{id}sends only{"directAccessGrantsEnabled": true}. Keycloak's clientPUTis a full replace — it will silently wipe all other client settings (redirect URIs, protocol mappers, scopes, etc.) not present in the payload. The helper needs toGETthe existing client config first and merge in the flag before sending thePUT.- Minor:
evaluate-benchmark.shcapturesPF_PROMETHEUS_LOGbut there is no startup check that surfaces its contents on failure, unlike the OTEL collector path.
Reviewed by clawgenti using the github-pr-review skill
| -X PUT "$KEYCLOAK_API/admin/realms/rossoctl/clients/$client_id" \ | ||
| -H "Authorization: Bearer $admin_token" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"directAccessGrantsEnabled": true}' 2>/dev/null) || put_code="000" |
There was a problem hiding this comment.
Bug: Keycloak PUT is a full replace, not a partial update.
Sending only {"directAccessGrantsEnabled": true} will silently wipe all other client settings (redirect URIs, protocol mappers, client scopes, etc.) that are not present in the payload.
The correct pattern:
local client_rep
client_rep=$(curl -s "$KEYCLOAK_API/admin/realms/rossoctl/clients/$client_id" \
-H "Authorization: Bearer $admin_token" 2>/dev/null)
client_rep=$(echo "$client_rep" | jq '.directAccessGrantsEnabled = true')
# Then PUT $client_rep instead of the hardcoded fragment… PUT
- analyze-run.sh: remove non-standard -save-traces single-dash alias;
keep only --save-traces (matches usage() and examples).
- keycloak-direct-access.sh: Keycloak treats PUT /clients/{id} as a
full-replace, so fetch the current client representation and merge
directAccessGrantsEnabled into it via jq instead of sending a bare
partial body that would reset unlisted fields. jq is now required.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Yoav Katz <katz@il.ibm.com>
clawgenti
left a comment
There was a problem hiding this comment.
This PR is a clean, well-considered refactor: the shared enable_direct_access_grants helper properly addresses the previous nits (full client PUT via jq merge, distinguishing unreachable Keycloak from bad creds). Two minor nits remain from the prior round.
- Nit (
libsh/keycloak-direct-access.shline 91): The PUT error response is captured into the hardcoded path/tmp/kc_put_response.txt. If the helper is ever called concurrently (or a leftover file exists from a previous run), this races/misleads. Amktemptemp file (cleaned up on exit) is safer. - Nit (
evaluate-benchmark.sh): Prometheus port-forward output is now captured toPF_PROMETHEUS_LOG, but unlike the OTEL collector path there is nops -p $PF_PROMETHEUS_PIDstartup check that surfaces the log contents on failure. The asymmetry means a Prometheus startup failure silently passes the health wait.
Reviewed by clawgenti using the github-pr-review skill
| exit 1 | ||
| fi | ||
|
|
||
| local put_code |
There was a problem hiding this comment.
Nit: /tmp/kc_put_response.txt is a hardcoded shared path — if two callers run concurrently, or a stale file exists from a prior failed invocation, the error message will reflect the wrong content. Consider local put_response_file; put_response_file=$(mktemp) and rm -f "$put_response_file" after use.
| "$KUBECTL_BIN" port-forward -n $PROMETHEUS_NAMESPACE svc/$PROMETHEUS_SERVICE ${PROMETHEUS_LOCAL_PORT}:9090 >/dev/null 2>&1 & | ||
| free_local_port "$PROMETHEUS_LOCAL_PORT" "Prometheus" | ||
| PF_PROMETHEUS_LOG="$(mktemp -t prom-pf.XXXXXX)" | ||
| "$KUBECTL_BIN" port-forward -n $PROMETHEUS_NAMESPACE svc/$PROMETHEUS_SERVICE ${PROMETHEUS_LOCAL_PORT}:9090 >"$PF_PROMETHEUS_LOG" 2>&1 & |
There was a problem hiding this comment.
Nit: PF_PROMETHEUS_LOG is now captured but the startup ps -p check only exists for the OTEL collector. Without a corresponding ps -p $PF_PROMETHEUS_PID check here that prints $PF_PROMETHEUS_LOG on failure, a silently-crashed Prometheus port-forward will go undetected until a later step fails with a confusing error.
Summary
deploy-agent.shanddeploy-benchmark.sh. It now lives in a newlibsh/keycloak-direct-access.shasenable_direct_access_grants, which resolves master-realm admin creds from env vars → thekeycloak-initial-adminsecret →admin/admindefaults, and exits with a clear diagnostic on any failure. An unreachable Keycloak (empty response) is now distinguished from a rejected credential.delete-all-deployments.sh. Its token requests usegrant_type=passwordagainst the rossoctl client, which requires Direct Access Grants to be enabled first. It now sources the helper and calls it before requesting a token.evaluate-benchmark.shport-forwards. Before binding a local port, detect a stale (or suspended) leftoverkubectl port-forwardholding it and clean it up — refusing to touch any non-kubectl process. Port-forward output is captured to a temp file so the real error is surfaced when startup fails; temp files are removed on cleanup.analyze-run.sh/download_mlflow_traces.pyMLflow auth. A distinct exit code 75 ("MLflow rejected the token") is raised viaMLflowAuthErrorand handled at theanalyze-run.shcall site, withurllib.errorcorrectly imported.analyze-run.shalso authenticates withgrant_type=passwordagainst the mlflow client (sourcing the shared helper), and gains a--save-tracesflag to persist downloaded traces.run-ibac-comparison.sh. New--kind/--openshift DOMAIN/--in-clustercluster flags and a--dryrun mode, all forwarded verbatim todeploy-and-evaluate.sh,delete-all-deployments.sh, andanalyze-run.sh(which already validate and support them).Testing
bash -nsyntax-checks pass on all changed scripts;py_compileclean on the Python.deploy-and-evaluate.shaccepts the forwarded--dry,--kind,--openshift, and--in-clusterflags.Review fixes
CLUSTER_FLAGto the finalanalyze-run.shcomparison call (previously missing, so--openshiftruns would target a Kind-style MLflow URL and fail after both benchmark runs completed).