Skip to content

Commit 4b25efc

Browse files
authored
Merge branch 'main' into konflux/mintmaker/main/konflux-base_image-quay.io-aipcc-base-images-cuda-12.9-el9.6
2 parents 07da158 + 0e0b540 commit 4b25efc

213 files changed

Lines changed: 9205 additions & 3080 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursor/rules/test-conventions.mdc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ alwaysApply: false
2424
| pytest | Test runner for all Python tests (subtests support is built in since pytest 9.0) |
2525
| pytest-cov | Coverage (XML + terminal) |
2626
| allure-pytest | Issue tracking + step decoration |
27+
| hypothesis | Property-based tests for pure helpers (`tests/unit/test_property_helpers.py`) |
28+
| crosshair | Optional SMT backend for Hypothesis (`make test-crosshair`; not default CI) |
2729
| testcontainers | Container lifecycle for integration tests |
2830
| docker (Python) | Low-level container operations |
2931
| pyfakefs | Filesystem mocking for unit tests |

.cursor/skills/arm64-rosa-gpu-smoke/SKILL.md

Lines changed: 629 additions & 0 deletions
Large diffs are not rendered by default.

.cursor/skills/arm64-rosa-gpu-smoke/scripts/gpu-manual-tests.py

Lines changed: 396 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env bash
2+
# Run ODH tests/manual GPU notebooks on all ARM CUDA images.
3+
# See ../SKILL.md Phase 3b. Uses non-interactive exec (oc exec -q equivalent).
4+
set -euo pipefail
5+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
6+
REPO_ROOT="$(cd "$(dirname "$0")/../../../.." && pwd)"
7+
cd "$REPO_ROOT"
8+
exec uv run "$ROOT/scripts/gpu-manual-tests.py" "$@"
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
#!/usr/bin/env bash
2+
# GPU smoke via Pod (no Notebook CR / RHOAI required). Run after nvidia.com/gpu allocatable.
3+
set -euo pipefail
4+
5+
: "${NS:?Set NS to a unique, dedicated namespace — this is a shared account, never default to a personal name}"
6+
: "${TAG:=rhoai-3.6-ea.1}"
7+
: "${PULL_SECRET:=rhoai-pull}"
8+
: "${TIMEOUT:=900}"
9+
: "${CLUSTER_CONTEXT:?Set CLUSTER_CONTEXT to the exact kubeconfig context (e.g. \$(oc config current-context) captured right after login) — never rely on the ambient current-context, which another process on this machine can change mid-session}"
10+
11+
IMG="${1:?usage: $0 <full-image-ref>}"
12+
13+
# NS/PULL_SECRET/IMG are spliced directly into the YAML heredoc below with
14+
# no serialization — reject anything that could break out of its scalar
15+
# context before that happens. NS/PULL_SECRET are also constrained to
16+
# valid Kubernetes names, which they need to be anyway. IMG must be a
17+
# single image-reference token — whitespace and "#" both open room for
18+
# a YAML comment or truncated value (e.g. "img # pytorch" would silently
19+
# truncate the interpolated image to "img").
20+
for _var_name in NS PULL_SECRET; do
21+
_val="${!_var_name}"
22+
[[ "$_val" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]] || { echo "ERROR: $_var_name '$_val' is not a valid Kubernetes name" >&2; exit 1; }
23+
done
24+
[[ "$IMG" =~ ^[^[:space:]#]+$ ]] || { echo "ERROR: IMG must be a single image-reference token (no whitespace or '#')" >&2; exit 1; }
25+
26+
hash_cmd() { command -v sha256sum >/dev/null 2>&1 && sha256sum || shasum -a 256; }
27+
timeout_cmd() { command -v timeout >/dev/null 2>&1 && echo timeout || command -v gtimeout >/dev/null 2>&1 && echo gtimeout || { echo "ERROR: need GNU timeout (brew install coreutils for gtimeout on macOS)" >&2; exit 1; }; }
28+
POD="gpu-smoke-$(printf '%s' "$IMG" | hash_cmd | cut -c1-16)-${RANDOM}${RANDOM}"
29+
30+
if [[ "$IMG" == *"-runtime-"* ]]; then
31+
IS_RUNTIME=1
32+
else
33+
IS_RUNTIME=0
34+
fi
35+
36+
if [[ "$IMG" == *"pytorch"* ]]; then
37+
LIB=torch
38+
elif [[ "$IMG" == *"tensorflow"* ]]; then
39+
LIB=tensorflow
40+
elif [[ "$IMG" == *"minimal-cuda"* ]]; then
41+
LIB=minimal
42+
else
43+
echo "Unknown image type for $IMG" >&2
44+
exit 1
45+
fi
46+
47+
cleanup() {
48+
oc --context "$CLUSTER_CONTEXT" delete pod "$POD" -n "$NS" --ignore-not-found --wait=true --timeout=30s >/dev/null 2>&1 || true
49+
sleep 3
50+
}
51+
trap cleanup EXIT
52+
53+
CMD='["sleep","infinity"]'
54+
if [[ "$IS_RUNTIME" -eq 0 ]]; then
55+
CMD='null'
56+
fi
57+
58+
cat <<EOF | oc --context "$CLUSTER_CONTEXT" apply -f -
59+
apiVersion: v1
60+
kind: Pod
61+
metadata:
62+
name: $POD
63+
namespace: $NS
64+
spec:
65+
restartPolicy: Never
66+
automountServiceAccountToken: false
67+
nodeSelector:
68+
kubernetes.io/arch: arm64
69+
tolerations:
70+
- key: nvidia.com/gpu
71+
operator: Exists
72+
effect: NoSchedule
73+
imagePullSecrets:
74+
- name: $PULL_SECRET
75+
containers:
76+
- name: smoke
77+
image: $IMG
78+
$(if [[ "$IS_RUNTIME" -eq 1 ]]; then
79+
cat <<INNER
80+
command: ["sleep", "infinity"]
81+
INNER
82+
fi)
83+
resources:
84+
limits:
85+
nvidia.com/gpu: "1"
86+
requests:
87+
nvidia.com/gpu: "1"
88+
securityContext:
89+
allowPrivilegeEscalation: false
90+
capabilities:
91+
drop: ["ALL"]
92+
runAsNonRoot: true
93+
seccompProfile:
94+
type: RuntimeDefault
95+
volumeMounts:
96+
- name: workspace
97+
mountPath: /opt/app-root/src
98+
volumes:
99+
- name: workspace
100+
emptyDir: {}
101+
EOF
102+
103+
echo "==> Waiting for pod $POD (image pull may take several minutes)..."
104+
if ! oc --context "$CLUSTER_CONTEXT" wait --for=condition=Ready "pod/$POD" -n "$NS" --timeout="${TIMEOUT}s"; then
105+
echo "FAIL: pod not ready" >&2
106+
oc --context "$CLUSTER_CONTEXT" describe pod "$POD" -n "$NS" | tail -20
107+
exit 1
108+
fi
109+
SCHEDULED_NODE=$(oc --context "$CLUSTER_CONTEXT" get pod "$POD" -n "$NS" -o jsonpath='{.spec.nodeName}')
110+
echo "==> Scheduled on node: $SCHEDULED_NODE"
111+
112+
case "$LIB" in
113+
torch)
114+
# if/raise, not assert — assert is a no-op under PYTHONOPTIMIZE, and a
115+
# SMOKE_PASS that can lie is worse than one that's merely verbose
116+
PY='import platform, torch
117+
if platform.machine() != "aarch64": raise SystemExit(platform.machine())
118+
if not torch.cuda.is_available(): raise SystemExit("cuda not available")
119+
print("device:", torch.cuda.get_device_name(0))
120+
print("sm:", torch.cuda.get_device_capability())
121+
x = torch.randn(1024, 1024, device="cuda")
122+
print("matmul_ok:", float((x @ x).mean().item()))
123+
print("SMOKE_PASS")'
124+
;;
125+
tensorflow)
126+
PY='import os, platform, tensorflow as tf
127+
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
128+
if platform.machine() != "aarch64": raise SystemExit(platform.machine())
129+
gpus = tf.config.list_physical_devices("GPU")
130+
print("gpus:", gpus)
131+
if not gpus: raise SystemExit("no GPU devices")
132+
tf.config.set_soft_device_placement(False)
133+
with tf.device("/GPU:0"):
134+
x = tf.random.uniform([1024, 1024])
135+
result = tf.matmul(x, x)
136+
if "GPU" not in result.device: raise SystemExit(result.device)
137+
print("SMOKE_PASS")'
138+
;;
139+
minimal)
140+
PY='import platform, subprocess
141+
if platform.machine() != "aarch64": raise SystemExit(platform.machine())
142+
out = subprocess.check_output(["nvidia-smi", "-L"], text=True)
143+
print(out.strip())
144+
if "GPU" not in out: raise SystemExit("no GPU in nvidia-smi -L output")
145+
print("SMOKE_PASS")'
146+
;;
147+
esac
148+
149+
echo "==> Running GPU check ($LIB)..."
150+
"$(timeout_cmd)" "${EXEC_TIMEOUT:-$TIMEOUT}s" oc --context "$CLUSTER_CONTEXT" exec -n "$NS" "$POD" -c smoke -- python -c "$PY"
151+
echo "==> PASS $IMG"
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env bash
2+
# Shared by rosa-hcp-provision and arm64-rosa-gpu-smoke: build a
3+
# dockerconfigjson pull secret from interactively-entered credentials and
4+
# create it in the cluster. Never accepts a credential as a CLI argument
5+
# (would leak via shell history / `ps`) and never reads
6+
# ~/.docker/config.json automatically (would silently harvest whatever
7+
# credential happens to be cached there).
8+
#
9+
# Usage: CLUSTER_CONTEXT=<ctx> create-pull-secret.sh <secret-name> <namespace> <registry-host-group> [<registry-host-group> ...]
10+
#
11+
# Each <registry-host-group> is one or more comma-separated auth keys that
12+
# share a single prompted credential, e.g. "quay.io,quay.io/rhoai" writes
13+
# the same auth under both keys (useful when a client does exact-key
14+
# lookup rather than prefix matching on the dockerconfigjson). Prompts for
15+
# a username, and skips the whole group entirely (no entry in the secret)
16+
# if the username is left blank — this is how a caller can conditionally
17+
# omit a registry it doesn't need (e.g. registry.redhat.io when only a
18+
# quay.io credential is available), without any special-cased flag. If a
19+
# username is given, the password is required non-empty.
20+
#
21+
# Requires CLUSTER_CONTEXT in the environment and passes it explicitly on
22+
# every oc call — never relies on / mutates the ambient current-context.
23+
# `oc config use-context` changes shared, machine-wide kubeconfig state; a
24+
# concurrent process changing it between the caller's setup and this
25+
# script's execution would otherwise create the secret in the wrong
26+
# cluster (see rosa-hcp-provision/SKILL.md's "always pass --context" rule).
27+
set -euo pipefail
28+
29+
: "${CLUSTER_CONTEXT:?Set CLUSTER_CONTEXT to the exact kubeconfig context of the target cluster}"
30+
31+
if [ "$#" -lt 3 ]; then
32+
echo "Usage: CLUSTER_CONTEXT=<ctx> $0 <secret-name> <namespace> <registry-host-group> [<registry-host-group> ...]" >&2
33+
exit 2
34+
fi
35+
36+
SECRET_NAME="$1"
37+
NAMESPACE="$2"
38+
shift 2
39+
40+
SECRET_FILE=$(umask 077 && mktemp)
41+
AUTH_FILE=""
42+
cleanup() { rm -f "$SECRET_FILE" "${AUTH_FILE:-}"; }
43+
trap cleanup EXIT
44+
45+
AUTHS_JSON="{}"
46+
ANY_HOST_CONFIGURED=false
47+
48+
for GROUP in "$@"; do
49+
read -r -p "Username for ${GROUP} (leave blank to skip this registry): " REG_USER
50+
if [ -z "$REG_USER" ]; then
51+
echo "Skipping ${GROUP} (no username given)" >&2
52+
continue
53+
fi
54+
read -rs -p "Password/token for ${GROUP}: " REG_PASS; echo
55+
if [ -z "$REG_PASS" ]; then
56+
echo "ERROR: a username was given for ${GROUP} but the password was empty" >&2
57+
exit 1
58+
fi
59+
AUTH_FILE=$(umask 077 && mktemp)
60+
printf '%s' "${REG_USER}:${REG_PASS}" | base64 | tr -d '\n' > "$AUTH_FILE"
61+
IFS=',' read -ra HOST_KEYS <<< "$GROUP"
62+
for HOST in "${HOST_KEYS[@]}"; do
63+
# --rawfile, not --arg $AUTH — --arg would put the (base64-encoded,
64+
# still sensitive) credential into jq's own process argument list.
65+
AUTHS_JSON=$(printf '%s' "$AUTHS_JSON" | jq --arg host "$HOST" --rawfile auth "$AUTH_FILE" \
66+
'.[$host] = {"auth": $auth}')
67+
done
68+
rm -f "$AUTH_FILE"
69+
ANY_HOST_CONFIGURED=true
70+
unset REG_USER REG_PASS AUTH_FILE
71+
done
72+
73+
if [ "$ANY_HOST_CONFIGURED" != true ]; then
74+
echo "ERROR: no registry host was configured (every username was left blank)" >&2
75+
exit 1
76+
fi
77+
78+
printf '%s' "$AUTHS_JSON" | jq '{"auths": .}' > "$SECRET_FILE"
79+
80+
oc --context "$CLUSTER_CONTEXT" create secret generic "$SECRET_NAME" -n "$NAMESPACE" \
81+
--from-file=.dockerconfigjson="$SECRET_FILE" \
82+
--type=kubernetes.io/dockerconfigjson \
83+
--dry-run=client -o yaml | oc --context "$CLUSTER_CONTEXT" apply -f -
84+
85+
rm -f "$SECRET_FILE"
86+
echo "Created/updated secret ${SECRET_NAME} in namespace ${NAMESPACE}" >&2

.cursor/skills/lib/wait-for-csv.sh

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env bash
2+
# Shared by rosa-hcp-provision docs: after creating a Subscription with
3+
# installPlanApproval: Manual, find the InstallPlan for an exact CSV
4+
# name (never `tail -1`/label-selector matching, which can grab an
5+
# unrelated or older InstallPlan when more than one exists in the
6+
# namespace), approve it, and wait for that CSV to succeed.
7+
#
8+
# Usage: CLUSTER_CONTEXT=<ctx> wait-for-csv.sh <namespace> <csv-name>
9+
#
10+
# Requires CLUSTER_CONTEXT in the environment and passes it explicitly on
11+
# every oc call — never relies on the ambient current-context, which is
12+
# shared, mutable, machine-wide state a concurrent process could change
13+
# between the caller's setup and this script's execution (see
14+
# rosa-hcp-provision/SKILL.md's "always pass --context" rule). Assumes the
15+
# Subscription referencing <csv-name> has already been applied (OLM
16+
# creates the InstallPlan asynchronously afterward, so this script polls
17+
# rather than querying once).
18+
set -euo pipefail
19+
20+
: "${CLUSTER_CONTEXT:?Set CLUSTER_CONTEXT to the exact kubeconfig context of the target cluster}"
21+
22+
if [ "$#" -ne 2 ]; then
23+
echo "Usage: CLUSTER_CONTEXT=<ctx> $0 <namespace> <csv-name>" >&2
24+
exit 2
25+
fi
26+
27+
NAMESPACE="$1"
28+
CSV_NAME="$2"
29+
30+
INSTALLPLAN=""
31+
for i in $(seq 1 12); do
32+
INSTALLPLAN=$(oc --context "$CLUSTER_CONTEXT" get installplan -n "$NAMESPACE" -o json | \
33+
jq -r --arg csv "$CSV_NAME" \
34+
'.items[] | select(.spec.clusterServiceVersionNames | index($csv)) | .metadata.name')
35+
MATCH_COUNT=$(printf '%s\n' "$INSTALLPLAN" | grep -c . || true)
36+
if [ "$MATCH_COUNT" -eq 1 ]; then
37+
break
38+
fi
39+
echo "Waiting for exactly one InstallPlan for ${CSV_NAME} (attempt ${i}/12, found ${MATCH_COUNT})..." >&2
40+
sleep 5
41+
done
42+
43+
MATCH_COUNT=$(printf '%s\n' "$INSTALLPLAN" | grep -c . || true)
44+
if [ "$MATCH_COUNT" -ne 1 ]; then
45+
echo "ERROR: expected exactly one InstallPlan for ${CSV_NAME} in ${NAMESPACE}, found ${MATCH_COUNT}" >&2
46+
exit 1
47+
fi
48+
49+
oc --context "$CLUSTER_CONTEXT" patch installplan "$INSTALLPLAN" -n "$NAMESPACE" --type merge -p '{"spec":{"approved":true}}'
50+
51+
# OLM creates the CSV object asynchronously after the InstallPlan is
52+
# approved. Poll for it to exist before starting the Succeeded-vs-Failed
53+
# race below — some oc/kubectl client versions return NotFound immediately
54+
# for `oc wait` against a not-yet-existing resource instead of waiting for
55+
# it to appear, which would otherwise let the race start handicapped.
56+
CSV_EXISTS=false
57+
for i in $(seq 1 12); do
58+
if oc --context "$CLUSTER_CONTEXT" get csv "$CSV_NAME" -n "$NAMESPACE" >/dev/null 2>&1; then
59+
CSV_EXISTS=true
60+
break
61+
fi
62+
echo "Waiting for csv/${CSV_NAME} to be created (attempt ${i}/12)..." >&2
63+
sleep 5
64+
done
65+
if [ "$CSV_EXISTS" != true ]; then
66+
echo "ERROR: csv/${CSV_NAME} in ${NAMESPACE} was not created within 60s of InstallPlan approval" >&2
67+
exit 1
68+
fi
69+
70+
# Race Succeeded against Failed instead of a single `--for=Succeeded` wait.
71+
# A CSV has a well-known binary failure signal (status.phase can reach
72+
# "Failed", distinct from "Succeeded") — unlike most resources (e.g. a
73+
# Pod, which has no single "give up early" condition), so this doesn't
74+
# need a heuristic: whichever phase is reached first is authoritative, and
75+
# the other wait is killed instead of blocking for the rest of the
76+
# 300s timeout after the outcome is already known. Portable to macOS's
77+
# stock bash 3.2 (no `wait -n`, which needs bash 4.3+) via a small polling
78+
# loop over two flag files instead.
79+
RESULT_DIR=$(mktemp -d)
80+
SUCCEEDED_PID=""
81+
FAILED_PID=""
82+
trap 'rm -rf "$RESULT_DIR"; kill "$SUCCEEDED_PID" "$FAILED_PID" 2>/dev/null || true' EXIT
83+
84+
(oc --context "$CLUSTER_CONTEXT" wait --for=jsonpath='{.status.phase}'=Succeeded "csv/${CSV_NAME}" -n "$NAMESPACE" --timeout=300s >/dev/null 2>&1 \
85+
&& touch "$RESULT_DIR/succeeded") &
86+
SUCCEEDED_PID=$!
87+
88+
(oc --context "$CLUSTER_CONTEXT" wait --for=jsonpath='{.status.phase}'=Failed "csv/${CSV_NAME}" -n "$NAMESPACE" --timeout=300s >/dev/null 2>&1 \
89+
&& touch "$RESULT_DIR/failed") &
90+
FAILED_PID=$!
91+
92+
while true; do
93+
if [ -f "$RESULT_DIR/succeeded" ]; then
94+
kill "$FAILED_PID" 2>/dev/null || true
95+
echo "csv/${CSV_NAME} in ${NAMESPACE} reached Succeeded" >&2
96+
exit 0
97+
fi
98+
if [ -f "$RESULT_DIR/failed" ]; then
99+
kill "$SUCCEEDED_PID" 2>/dev/null || true
100+
echo "ERROR: csv/${CSV_NAME} in ${NAMESPACE} reached Failed" >&2
101+
oc --context "$CLUSTER_CONTEXT" get csv "$CSV_NAME" -n "$NAMESPACE" -o jsonpath='{.status.message}{"\n"}' >&2 || true
102+
exit 1
103+
fi
104+
if ! kill -0 "$SUCCEEDED_PID" 2>/dev/null && ! kill -0 "$FAILED_PID" 2>/dev/null; then
105+
echo "ERROR: csv/${CSV_NAME} in ${NAMESPACE} reached neither Succeeded nor Failed within 300s" >&2
106+
exit 1
107+
fi
108+
sleep 2
109+
done

0 commit comments

Comments
 (0)