-
Notifications
You must be signed in to change notification settings - Fork 68
OCPMCP-355: Add keycloak setup for prow/openshift CI #469
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,12 +31,35 @@ import ( | |
| const ( | ||
| keycloakRealm = "openshift" | ||
|
|
||
| // keycloakDefaultBaseURL is the internal cluster URL for Keycloak when no | ||
| // override is set. On Minikube this is the only URL; on OCP the env var | ||
| // KEYCLOAK_ISSUER_URL overrides it to the Route URL so the kube-apiserver | ||
| // (which runs on host networking) can reach Keycloak. | ||
| keycloakDefaultBaseURL = "https://keycloak.keycloak.svc:8443" | ||
|
|
||
| // caSecretName is the secret holding the CA cert the MCP server trusts when | ||
| // talking to Keycloak; caMountPath is where the chart mounts it in the pod. | ||
| caSecretName = "keycloak-ca" | ||
| caMountPath = "/etc/keycloak-ca" | ||
| ) | ||
|
|
||
| // keycloakIssuerURL returns the Keycloak issuer URL (base + realm) used in | ||
| // OIDC configuration and token issuer assertions. On OCP CI this is overridden | ||
| // via KEYCLOAK_ISSUER_URL to the Route URL. | ||
| func keycloakIssuerURL() string { | ||
| base := envOrDefault("KEYCLOAK_ISSUER_URL", keycloakDefaultBaseURL+"/realms/"+keycloakRealm) | ||
| return base | ||
| } | ||
|
|
||
| // keycloakBaseURL returns just the Keycloak base URL (without /realms/...). | ||
| func keycloakBaseURL() string { | ||
| issuer := keycloakIssuerURL() | ||
| if idx := strings.Index(issuer, "/realms/"); idx != -1 { | ||
| return issuer[:idx] | ||
| } | ||
| return envOrDefault("KEYCLOAK_BASE_URL", keycloakDefaultBaseURL) | ||
| } | ||
|
|
||
| // oidcDiscovery is the subset of the OIDC discovery document the tests use. | ||
| type oidcDiscovery struct { | ||
| Issuer string `json:"issuer"` | ||
|
|
@@ -245,17 +268,33 @@ func mcpViewerToken(t *testing.T, keycloakURL string, scopes ...string) string { | |
| }) | ||
| } | ||
|
|
||
| // copyKeycloakCASecret copies the cert-manager self-signed CA into the test | ||
| // namespace as the caSecretName secret so the MCP server pod can mount it and | ||
| // trust Keycloak's TLS. Intended as a deployServer preInstall hook. | ||
| // copyKeycloakCASecret copies the CA certificate that the MCP server needs to | ||
| // trust Keycloak's TLS into the test namespace. On Minikube this is the | ||
| // cert-manager self-signed CA (from cert-manager/selfsigned-ca-secret). On OCP | ||
| // CI the env var KEYCLOAK_CA_SECRET can override the source to a pre-created | ||
| // secret (e.g. the OpenShift ingress CA). The format is "namespace/name". | ||
| func copyKeycloakCASecret(ctx context.Context, t *testing.T, clientset kubernetes.Interface, namespace string) { | ||
| t.Helper() | ||
| caSecret, err := clientset.CoreV1().Secrets("cert-manager").Get(ctx, "selfsigned-ca-secret", metav1.GetOptions{}) | ||
| require.NoError(t, err, "get cert-manager CA secret") | ||
|
|
||
| _, err = clientset.CoreV1().Secrets(namespace).Create(ctx, &corev1.Secret{ | ||
| var caCert []byte | ||
| if src := os.Getenv("KEYCLOAK_CA_SECRET"); src != "" { | ||
| parts := strings.SplitN(src, "/", 2) | ||
| require.Len(t, parts, 2, "KEYCLOAK_CA_SECRET must be namespace/name, got %q", src) | ||
| secret, err := clientset.CoreV1().Secrets(parts[0]).Get(ctx, parts[1], metav1.GetOptions{}) | ||
| require.NoError(t, err, "get CA secret %s", src) | ||
| for _, v := range secret.Data { | ||
| caCert = v | ||
| break | ||
|
Comment on lines
+285
to
+287
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
setup="$(fd -a '^keycloak-setup\.sh$' test | head -n 1)"
test -n "$setup"
rg -n -C 5 'KEYCLOAK_CA_SECRET|ca\.crt|tls\.crt|tls\.key|create secret' "$setup"Repository: openshift/openshift-mcp-server Length of output: 3280 🏁 Script executed: #!/bin/bash
set -euo pipefail
file="test/e2e/oauth_test.go"
test -f "$file"
sed -n '240,315p' "$file"
printf '\n--- bindings and callers ---\n'
rg -n -C 4 'func copyKeycloakCASecret|copyKeycloakCASecret\(|KEYCLOAK_CA_SECRET|keycloak-ingress-ca' test/e2e "$file"
printf '\n--- require import ---\n'
sed -n '1,45p' "$file"Repository: openshift/openshift-mcp-server Length of output: 8305 Select The 🤖 Prompt for AI Agents |
||
| } | ||
| } else { | ||
| caSecret, err := clientset.CoreV1().Secrets("cert-manager").Get(ctx, "selfsigned-ca-secret", metav1.GetOptions{}) | ||
| require.NoError(t, err, "get cert-manager CA secret") | ||
| caCert = caSecret.Data["ca.crt"] | ||
| } | ||
|
|
||
| _, err := clientset.CoreV1().Secrets(namespace).Create(ctx, &corev1.Secret{ | ||
| ObjectMeta: metav1.ObjectMeta{Name: caSecretName}, | ||
| Data: map[string][]byte{"ca.crt": caSecret.Data["ca.crt"]}, | ||
| Data: map[string][]byte{"ca.crt": caCert}, | ||
| }, metav1.CreateOptions{}) | ||
| require.NoError(t, err, "create CA secret in test namespace") | ||
| } | ||
|
|
@@ -443,13 +482,14 @@ func oidcServerConfig(oauthScopes []string) string { | |
| oauth_audience = "mcp-server" | ||
| oauth_scopes = %s | ||
| validate_token = false | ||
| authorization_url = "https://keycloak.keycloak.svc:8443/realms/openshift" | ||
| authorization_url = "%s" | ||
| sts_client_id = "mcp-server" | ||
| sts_client_secret = "mcp-server-dev-secret" | ||
| sts_audience = "openshift" | ||
| sts_scopes = ["mcp:openshift"] | ||
| certificate_authority = "%s/ca.crt" | ||
| `, scopes, caMountPath) | ||
| denied_resources = [] | ||
| `, scopes, keycloakIssuerURL(), caMountPath) | ||
| } | ||
|
|
||
| // tomlStringArray renders a Go string slice as a TOML array literal. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # RBAC bindings for Keycloak OIDC users on OpenShift. | ||
| # | ||
| # On OCP the Authentication CR uses prefixPolicy: NoPrefix, so usernames | ||
| # are bare (e.g. "mcp") rather than "<issuer-url>#mcp" as on Minikube. | ||
| apiVersion: rbac.authorization.k8s.io/v1 | ||
| kind: ClusterRoleBinding | ||
| metadata: | ||
| name: oidc-mcp-cluster-admin | ||
| roleRef: | ||
| apiGroup: rbac.authorization.k8s.io | ||
| kind: ClusterRole | ||
| name: cluster-admin | ||
| subjects: | ||
| - apiGroup: rbac.authorization.k8s.io | ||
| kind: User | ||
| name: mcp | ||
| --- | ||
| apiVersion: rbac.authorization.k8s.io/v1 | ||
| kind: ClusterRoleBinding | ||
| metadata: | ||
| name: oidc-mcp-viewers-view | ||
| roleRef: | ||
| apiGroup: rbac.authorization.k8s.io | ||
| kind: ClusterRole | ||
| name: view | ||
| subjects: | ||
| - apiGroup: rbac.authorization.k8s.io | ||
| kind: Group | ||
| name: mcp-viewers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Assert the exact configured issuer URL.
Contains(..., "keycloak")accepts a different Keycloak hostname. The test can pass when discovery returns an issuer that differs from the Route issuer used by OpenShift and the MCP server. Compareissuerord.IssuerwithkeycloakIssuerURL().test/e2e/oauth_flows_test.go#L121-L121: replace the substring assertion with an equality assertion againstkeycloakIssuerURL().test/e2e/keycloak_test.go#L32-L33: replace the substring assertion with an equality assertion againstkeycloakIssuerURL().📍 Affects 2 files
test/e2e/oauth_flows_test.go#L121-L121(this comment)test/e2e/keycloak_test.go#L32-L33🤖 Prompt for AI Agents