|
| 1 | +import json |
1 | 2 | import os |
2 | 3 | import random |
3 | 4 | import string |
@@ -142,6 +143,141 @@ def get_setup_env_variables(**kwargs): |
142 | 143 | return env_vars |
143 | 144 |
|
144 | 145 |
|
| 146 | +def _env_flag_enabled(name): |
| 147 | + return os.environ.get(name, "").strip().lower() in ("1", "true", "yes") |
| 148 | + |
| 149 | + |
| 150 | +def _env_flag_disabled(name): |
| 151 | + return os.environ.get(name, "").strip().lower() in ("0", "false", "no", "off") |
| 152 | + |
| 153 | + |
| 154 | +def _has_registry_mirror_configured(): |
| 155 | + """ |
| 156 | + Detect mirror-only registry layout typical of disconnected OpenShift installs. |
| 157 | + """ |
| 158 | + try: |
| 159 | + custom_api = client.CustomObjectsApi() |
| 160 | + mirror_types = [ |
| 161 | + ("config.openshift.io", "v1", "imagedigestmirrorsets"), |
| 162 | + ("operator.openshift.io", "v1alpha1", "imagecontentsourcepolicies"), |
| 163 | + ] |
| 164 | + for group, version, plural in mirror_types: |
| 165 | + try: |
| 166 | + result = custom_api.list_cluster_custom_object( |
| 167 | + group=group, |
| 168 | + version=version, |
| 169 | + plural=plural, |
| 170 | + ) |
| 171 | + if result.get("items"): |
| 172 | + return True |
| 173 | + except Exception: |
| 174 | + continue |
| 175 | + except Exception: |
| 176 | + pass |
| 177 | + return False |
| 178 | + |
| 179 | + |
| 180 | +def _disconnected_cluster_signals(): |
| 181 | + """Return True when the cluster is likely disconnected / air-gapped.""" |
| 182 | + if _env_flag_enabled("DISCONNECTED_CLUSTER") or _env_flag_enabled( |
| 183 | + "IS_DISCONNECTED_CLUSTER" |
| 184 | + ): |
| 185 | + return True |
| 186 | + if _has_registry_mirror_configured(): |
| 187 | + return True |
| 188 | + try: |
| 189 | + server = (run_oc_command(["whoami", "--show-server=true"]) or "").lower() |
| 190 | + if "-dis-" in server or "disconnected" in server: |
| 191 | + return True |
| 192 | + except Exception: |
| 193 | + pass |
| 194 | + return False |
| 195 | + |
| 196 | + |
| 197 | +def _upgrade_mnist_prerequisites_met(): |
| 198 | + """ |
| 199 | + Return True when full MNIST can run (pip packages + dataset reachable). |
| 200 | + """ |
| 201 | + if not _disconnected_cluster_signals(): |
| 202 | + return True |
| 203 | + |
| 204 | + pip_url = (os.environ.get("PIP_INDEX_URL") or "").strip() |
| 205 | + aws_endpoint = (os.environ.get("AWS_DEFAULT_ENDPOINT") or "").strip() |
| 206 | + pip_ok = bool(pip_url) and "pypi.org" not in pip_url |
| 207 | + aws_ok = bool(aws_endpoint) |
| 208 | + if pip_ok and aws_ok: |
| 209 | + print( |
| 210 | + "Disconnected cluster with PIP mirror and S3 endpoint configured; " |
| 211 | + "using full MNIST upgrade job" |
| 212 | + ) |
| 213 | + return True |
| 214 | + return False |
| 215 | + |
| 216 | + |
| 217 | +def use_upgrade_smoke_job(): |
| 218 | + """ |
| 219 | + Use a lightweight Ray job for upgrade tests when full MNIST is not viable. |
| 220 | + """ |
| 221 | + if _env_flag_enabled("UPGRADE_USE_SMOKE_JOB"): |
| 222 | + print("UPGRADE_USE_SMOKE_JOB enabled; using upgrade smoke job") |
| 223 | + return True |
| 224 | + if _env_flag_disabled("UPGRADE_USE_SMOKE_JOB"): |
| 225 | + print("UPGRADE_USE_SMOKE_JOB disabled; using full MNIST upgrade job") |
| 226 | + return False |
| 227 | + |
| 228 | + if _upgrade_mnist_prerequisites_met(): |
| 229 | + return False |
| 230 | + |
| 231 | + if _env_flag_enabled("DISCONNECTED_CLUSTER") or _env_flag_enabled( |
| 232 | + "IS_DISCONNECTED_CLUSTER" |
| 233 | + ): |
| 234 | + print( |
| 235 | + "Disconnected cluster env set without PIP/S3 mirrors; " |
| 236 | + "using upgrade smoke job (no pip install)" |
| 237 | + ) |
| 238 | + return True |
| 239 | + |
| 240 | + if _has_registry_mirror_configured(): |
| 241 | + print( |
| 242 | + "Registry mirror detected (ImageDigestMirrorSet/ICSP) without " |
| 243 | + "PIP/S3 mirrors; using upgrade smoke job (no pip install)" |
| 244 | + ) |
| 245 | + return True |
| 246 | + |
| 247 | + try: |
| 248 | + server = (run_oc_command(["whoami", "--show-server=true"]) or "").lower() |
| 249 | + if "-dis-" in server or "disconnected" in server: |
| 250 | + print( |
| 251 | + "Detected disconnected cluster from API server URL; " |
| 252 | + "using upgrade smoke job (no pip install)" |
| 253 | + ) |
| 254 | + return True |
| 255 | + except Exception: |
| 256 | + pass |
| 257 | + |
| 258 | + return False |
| 259 | + |
| 260 | + |
| 261 | +def get_upgrade_job_submission_spec(): |
| 262 | + """Return entrypoint and runtime_env for post-upgrade job submission tests.""" |
| 263 | + if use_upgrade_smoke_job(): |
| 264 | + return { |
| 265 | + "entrypoint": "python upgrade_job_smoke.py", |
| 266 | + "runtime_env": { |
| 267 | + "working_dir": "./tests/e2e/", |
| 268 | + "env_vars": get_setup_env_variables(), |
| 269 | + }, |
| 270 | + } |
| 271 | + return { |
| 272 | + "entrypoint": "python mnist.py", |
| 273 | + "runtime_env": { |
| 274 | + "working_dir": "./tests/e2e/", |
| 275 | + "pip": "./tests/e2e/mnist_pip_requirements.txt", |
| 276 | + "env_vars": get_setup_env_variables(), |
| 277 | + }, |
| 278 | + } |
| 279 | + |
| 280 | + |
145 | 281 | def random_choice(): |
146 | 282 | alphabet = string.ascii_lowercase + string.digits |
147 | 283 | return "".join(random.choices(alphabet, k=5)) |
@@ -1399,3 +1535,118 @@ def verify_network_policy_spec( |
1399 | 1535 | result["allowed_ports"].append(port.port) |
1400 | 1536 |
|
1401 | 1537 | return result |
| 1538 | + |
| 1539 | + |
| 1540 | +def is_byoidc_cluster_detected(): |
| 1541 | + """ |
| 1542 | + BYOIDC cluster detection by checking OpenShift cluster Authentication resource. |
| 1543 | + """ |
| 1544 | + try: |
| 1545 | + custom_api = client.CustomObjectsApi() |
| 1546 | + auth_resource = custom_api.get_cluster_custom_object( |
| 1547 | + group="config.openshift.io", |
| 1548 | + version="v1", |
| 1549 | + plural="authentications", |
| 1550 | + name="cluster", |
| 1551 | + ) |
| 1552 | + |
| 1553 | + spec = auth_resource.get("spec", {}) |
| 1554 | + |
| 1555 | + if (spec.get("type") or "").upper() == "OIDC": |
| 1556 | + print("Detected BYOIDC cluster: Authentication spec.type is OIDC") |
| 1557 | + return True |
| 1558 | + |
| 1559 | + if "oidcProviders" in spec and spec["oidcProviders"]: |
| 1560 | + for provider in spec["oidcProviders"]: |
| 1561 | + issuer_url = provider.get("issuer", {}).get("issuerURL", "") |
| 1562 | + if "keycloak" in issuer_url.lower() and ( |
| 1563 | + "rh-ods.com" in issuer_url or "qe.rh-ods.com" in issuer_url |
| 1564 | + ): |
| 1565 | + print(f"Detected BYOIDC cluster with OIDC issuer: {issuer_url}") |
| 1566 | + return True |
| 1567 | + |
| 1568 | + if spec.get("webhookTokenAuthenticators"): |
| 1569 | + for webhook in spec["webhookTokenAuthenticators"]: |
| 1570 | + if webhook.get("kubeConfig", {}): |
| 1571 | + print("Detected BYOIDC cluster with webhook token authenticator") |
| 1572 | + return True |
| 1573 | + |
| 1574 | + status = auth_resource.get("status", {}) |
| 1575 | + oidc_clients_blob = json.dumps(status.get("oidcClients", [])) |
| 1576 | + if "oc-cli" in oidc_clients_blob: |
| 1577 | + print("Detected BYOIDC cluster from status.oidcClients (oc-cli client)") |
| 1578 | + return True |
| 1579 | + |
| 1580 | + print("No BYOIDC indicators found in cluster Authentication resource") |
| 1581 | + return False |
| 1582 | + |
| 1583 | + except Exception as e: |
| 1584 | + print(f"Could not check cluster authentication method: {e}") |
| 1585 | + return False |
| 1586 | + |
| 1587 | + |
| 1588 | +def get_byoidc_issuer_url(): |
| 1589 | + """Get OIDC issuer URL from cluster Authentication resource.""" |
| 1590 | + try: |
| 1591 | + custom_api = client.CustomObjectsApi() |
| 1592 | + auth_resource = custom_api.get_cluster_custom_object( |
| 1593 | + group="config.openshift.io", |
| 1594 | + version="v1", |
| 1595 | + plural="authentications", |
| 1596 | + name="cluster", |
| 1597 | + ) |
| 1598 | + |
| 1599 | + spec = auth_resource.get("spec", {}) |
| 1600 | + if "oidcProviders" in spec and spec["oidcProviders"]: |
| 1601 | + for provider in spec["oidcProviders"]: |
| 1602 | + issuer_url = provider.get("issuer", {}).get("issuerURL", "") |
| 1603 | + if issuer_url: |
| 1604 | + return issuer_url |
| 1605 | + |
| 1606 | + return "https://keycloak.qe.rh-ods.com" |
| 1607 | + |
| 1608 | + except Exception as e: |
| 1609 | + print(f"Could not get OIDC issuer URL from cluster: {e}") |
| 1610 | + return "https://keycloak.qe.rh-ods.com" |
| 1611 | + |
| 1612 | + |
| 1613 | +def get_oidc_tokens(username, password, issuer_url): |
| 1614 | + """Get OIDC tokens (id_token and refresh_token) for a user.""" |
| 1615 | + try: |
| 1616 | + import requests |
| 1617 | + |
| 1618 | + if "/realms/" in issuer_url: |
| 1619 | + token_url = f"{issuer_url}/protocol/openid-connect/token" |
| 1620 | + else: |
| 1621 | + token_url = f"{issuer_url}/realms/openshift/protocol/openid-connect/token" |
| 1622 | + |
| 1623 | + print(f"Requesting OIDC tokens from: {token_url}") |
| 1624 | + |
| 1625 | + data = { |
| 1626 | + "grant_type": "password", |
| 1627 | + "client_id": "oc-cli", |
| 1628 | + "username": username, |
| 1629 | + "password": password, |
| 1630 | + "scope": "openid profile email", |
| 1631 | + } |
| 1632 | + |
| 1633 | + response = requests.post(token_url, data=data, verify=False, timeout=30) |
| 1634 | + |
| 1635 | + if response.status_code == 200: |
| 1636 | + token_data = response.json() |
| 1637 | + id_token = token_data.get("id_token") |
| 1638 | + refresh_token = token_data.get("refresh_token") |
| 1639 | + |
| 1640 | + if id_token and refresh_token: |
| 1641 | + print("✓ Successfully obtained OIDC tokens") |
| 1642 | + return id_token, refresh_token |
| 1643 | + print("ERROR: Token response missing id_token or refresh_token") |
| 1644 | + return None, None |
| 1645 | + |
| 1646 | + print(f"ERROR: Token request failed with status {response.status_code}") |
| 1647 | + print(f"Response: {response.text}") |
| 1648 | + return None, None |
| 1649 | + |
| 1650 | + except Exception as e: |
| 1651 | + print(f"Error getting OIDC tokens: {e}") |
| 1652 | + return None, None |
0 commit comments