Skip to content

Commit 29cbdba

Browse files
RHOAIENG-69301: fix disconnected env job submission (3.3)
1 parent 37cbe80 commit 29cbdba

3 files changed

Lines changed: 326 additions & 23 deletions

File tree

tests/e2e/support.py

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import os
23
import random
34
import string
@@ -142,6 +143,141 @@ def get_setup_env_variables(**kwargs):
142143
return env_vars
143144

144145

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+
145281
def random_choice():
146282
alphabet = string.ascii_lowercase + string.digits
147283
return "".join(random.choices(alphabet, k=5))
@@ -1399,3 +1535,118 @@ def verify_network_policy_spec(
13991535
result["allowed_ports"].append(port.port)
14001536

14011537
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

tests/e2e/upgrade_job_smoke.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright 2024 IBM, Red Hat
2+
#
3+
# Minimal Ray job for upgrade qualification on disconnected clusters.
4+
# Validates job submission and execution without pip installs or external datasets.
5+
6+
import sys
7+
8+
9+
def main() -> int:
10+
print("upgrade-job-smoke: job started")
11+
print("upgrade-job-smoke: job finished successfully")
12+
return 0
13+
14+
15+
if __name__ == "__main__":
16+
sys.exit(main())

0 commit comments

Comments
 (0)