-
Notifications
You must be signed in to change notification settings - Fork 2.3k
INTEROP-9411: Replace Quay UI smoke tests with cross-product interop validation #83360
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
Changes from all commits
42ac761
e461840
4eee7bc
06f097e
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 |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| approvers: | ||
| - cspi-qe-ocp-lp | ||
| reviewers: | ||
| - cspi-qe-ocp-lp |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,334 @@ | ||
| #!/bin/bash | ||
| set -euo pipefail | ||
| shopt -s inherit_errexit | ||
|
|
||
| ARTIFACT_DIR="${ARTIFACT_DIR:=/tmp/artifacts}" | ||
| mkdir -p "${ARTIFACT_DIR}" | ||
| typeset junitFile="${ARTIFACT_DIR}/junit_quay_interop.xml" | ||
| typeset imageTag="${BUILD_ID:-$(date +%s)}" | ||
|
|
||
| typeset -A testStatus | ||
| typeset -A testDuration | ||
| typeset -A testFailureMsg | ||
| typeset -a allTests=( | ||
| "[sig-interop][Jira:INTEROP][Feature:Quay] Push and pull image via Quay route" | ||
| "[sig-interop][Jira:INTEROP][Feature:Quay] Verify ODF PVC backing Quay storage" | ||
| "[sig-interop][Jira:INTEROP][Feature:Quay] ACS scan of pushed Quay image" | ||
| ) | ||
|
|
||
| for t in "${allTests[@]}"; do | ||
| testStatus["${t}"]="skipped" | ||
| testDuration["${t}"]=0 | ||
| testFailureMsg["${t}"]="Test did not run" | ||
| done | ||
|
|
||
| typeset -i suiteStart=0 | ||
| suiteStart=$(date +%s) | ||
|
|
||
| function RecordResult () { | ||
| typeset name="${1}"; shift | ||
| typeset status="${1}"; shift | ||
| typeset msg="${1:-}"; shift || true | ||
| typeset dur="${1:-0}"; shift || true | ||
| testStatus["${name}"]="${status}" | ||
| testDuration["${name}"]="${dur}" | ||
| testFailureMsg["${name}"]="${msg}" | ||
| } | ||
|
|
||
| # shellcheck disable=SC2329 | ||
| function GenerateJunit () { | ||
| typeset -i total=${#allTests[@]} | ||
| typeset -i failures=0 skipped=0 | ||
| typeset -i elapsed=$(( $(date +%s) - suiteStart )) | ||
|
|
||
| for t in "${allTests[@]}"; do | ||
| [[ "${testStatus[${t}]}" == "failed" ]] && failures=$((failures + 1)) | ||
| [[ "${testStatus[${t}]}" == "skipped" ]] && skipped=$((skipped + 1)) | ||
| done | ||
|
|
||
| cat > "${junitFile}" <<EOF | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <testsuites> | ||
| <testsuite name="interop-tests-opp-quay-smoke" tests="${total}" failures="${failures}" errors="0" skipped="${skipped}" time="${elapsed}"> | ||
| EOF | ||
|
|
||
| for t in "${allTests[@]}"; do | ||
| typeset escaped_name | ||
| escaped_name=$(printf '%s' "${t}" | sed 's/&/\&/g; s/</\</g; s/>/\>/g; s/"/\"/g') | ||
| typeset escaped_msg | ||
| escaped_msg=$(printf '%s' "${testFailureMsg[${t}]}" | sed 's/&/\&/g; s/</\</g; s/>/\>/g; s/"/\"/g') | ||
|
|
||
| if [[ "${testStatus[${t}]}" == "failed" ]]; then | ||
| echo " <testcase name=\"${escaped_name}\" classname=\"interop-tests-opp-quay-smoke\" time=\"${testDuration[${t}]}\"><failure message=\"${escaped_msg}\"><![CDATA[${testFailureMsg[${t}]}]]></failure></testcase>" >> "${junitFile}" | ||
| elif [[ "${testStatus[${t}]}" == "skipped" ]]; then | ||
| echo " <testcase name=\"${escaped_name}\" classname=\"interop-tests-opp-quay-smoke\" time=\"${testDuration[${t}]}\"><skipped message=\"${escaped_msg}\"/></testcase>" >> "${junitFile}" | ||
| else | ||
| echo " <testcase name=\"${escaped_name}\" classname=\"interop-tests-opp-quay-smoke\" time=\"${testDuration[${t}]}\"/>" >> "${junitFile}" | ||
| fi | ||
| done | ||
|
|
||
| cat >> "${junitFile}" <<EOF | ||
| </testsuite> | ||
| </testsuites> | ||
| EOF | ||
| cat "${junitFile}" | ||
| } | ||
|
|
||
| trap GenerateJunit EXIT | ||
|
|
||
| function DiscoverQuay () { | ||
| QUAY_NS=$(oc get quayregistry --all-namespaces -o jsonpath='{.items[0].metadata.namespace}') | ||
| QUAY_REGISTRY=$(oc get quayregistry -n "${QUAY_NS}" -o jsonpath='{.items[0].metadata.name}') | ||
| QUAY_HOST=$(oc get quayregistry -n "${QUAY_NS}" "${QUAY_REGISTRY}" -o jsonpath='{.status.registryEndpoint}') | ||
| QUAY_HOST="${QUAY_HOST#https://}" | ||
| export QUAY_NS QUAY_REGISTRY QUAY_HOST | ||
| } | ||
|
|
||
| function GetQuayAuth () { | ||
| typeset configSecret | ||
| configSecret=$(oc get quayregistry -n "${QUAY_NS}" "${QUAY_REGISTRY}" -o jsonpath='{.spec.configBundleSecret}') | ||
| if [[ -z "${configSecret}" ]]; then | ||
| configSecret="${QUAY_REGISTRY}-config-bundle" | ||
| fi | ||
|
|
||
| QUAY_USER=$(oc get secret -n "${QUAY_NS}" "${configSecret}" -o jsonpath='{.data.SUPER_USER_EMAIL}' 2>/dev/null | base64 -d || echo "") | ||
| if [[ -z "${QUAY_USER}" ]]; then | ||
| QUAY_USER="quayadmin" | ||
| fi | ||
| QUAY_PASSWORD=$(oc get secret -n "${QUAY_NS}" "${configSecret}" -o jsonpath='{.data.SUPER_USER_PASSWORD}' 2>/dev/null | base64 -d || echo "") | ||
|
|
||
| if [[ -z "${QUAY_PASSWORD}" ]]; then | ||
| typeset initSecret="${QUAY_REGISTRY}-init-config-bundle-secret" | ||
| QUAY_PASSWORD=$(oc get secret -n "${QUAY_NS}" "${initSecret}" -o jsonpath='{.data.superuser-password}' 2>/dev/null | base64 -d || echo "") | ||
| fi | ||
|
|
||
| if [[ -z "${QUAY_PASSWORD}" ]]; then | ||
| for secret in $(oc get secrets -n "${QUAY_NS}" -o jsonpath='{.items[*].metadata.name}' | tr ' ' '\n' | grep -i "quay.*config"); do | ||
| QUAY_PASSWORD=$(oc get secret -n "${QUAY_NS}" "${secret}" -o go-template='{{index .data "config.yaml"}}' 2>/dev/null | base64 -d | grep -oP "(?<=SUPER_USER_PASSWORD: ).*" || echo "") | ||
| [[ -n "${QUAY_PASSWORD}" ]] && break | ||
| done | ||
| fi | ||
|
|
||
| export QUAY_USER QUAY_PASSWORD | ||
| } | ||
|
|
||
| function PreflightCheck () { | ||
| if ! curl -sk --connect-timeout 15 "https://${QUAY_HOST}/api/v1/discovery" | grep -qi "quay"; then | ||
| echo "ERROR: Quay route not reachable at ${QUAY_HOST}" >&2 | ||
| return 1 | ||
| fi | ||
| } | ||
|
|
||
| function CreateTestOrg () { | ||
| typeset signinPayload | ||
| signinPayload=$(python3 -c "import json,sys; print(json.dumps({'user':sys.argv[1],'pass':sys.argv[2]}))" "${QUAY_USER}" "${QUAY_PASSWORD}") | ||
| typeset token | ||
| token=$(curl -sk -X POST "https://${QUAY_HOST}/api/v1/signin" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d "${signinPayload}" | \ | ||
| python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null || echo "") | ||
|
|
||
| if [[ -z "${token}" ]]; then | ||
| token=$(curl -sk -H "Authorization: Basic $(echo -n "${QUAY_USER}:${QUAY_PASSWORD}" | base64)" \ | ||
| "https://${QUAY_HOST}/api/v1/user/" | \ | ||
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('token',''))" 2>/dev/null || echo "") | ||
| fi | ||
|
|
||
| QUAY_TOKEN="${token}" | ||
| export QUAY_TOKEN | ||
|
|
||
| curl -sk -X POST "https://${QUAY_HOST}/api/v1/organization/" \ | ||
| -H "Authorization: Bearer ${QUAY_TOKEN}" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"name":"interop-smoke-test","email":"interop-test@example.com"}' || true | ||
| } | ||
|
|
||
| ################################################################################ | ||
| # Test Case 1: Push and pull image via Quay route | ||
| ################################################################################ | ||
| function RunPushPull () { | ||
| typeset testName="[sig-interop][Jira:INTEROP][Feature:Quay] Push and pull image via Quay route" | ||
| typeset -i start elapsed | ||
| start=$(date +%s) | ||
|
|
||
| typeset pushTarget="${QUAY_HOST}/interop-smoke-test/ubi-smoke:${imageTag}" | ||
| typeset authFile="/tmp/quay-auth.json" | ||
|
|
||
| cat > "${authFile}" <<EOF | ||
| {"auths":{"${QUAY_HOST}":{"auth":"$(echo -n "${QUAY_USER}:${QUAY_PASSWORD}" | base64)"}}} | ||
| EOF | ||
|
|
||
| if ! skopeo copy --dest-tls-verify=false \ | ||
| --dest-authfile="${authFile}" \ | ||
| docker://registry.access.redhat.com/ubi9-minimal:latest \ | ||
| "docker://${pushTarget}" 2>&1; then | ||
| elapsed=$(( $(date +%s) - start )) | ||
| RecordResult "${testName}" "failed" "skopeo push to Quay failed" "${elapsed}" | ||
| return 1 | ||
| fi | ||
|
|
||
| if ! skopeo inspect --tls-verify=false \ | ||
| --authfile="${authFile}" \ | ||
| "docker://${pushTarget}" >/dev/null 2>&1; then | ||
| elapsed=$(( $(date +%s) - start )) | ||
| RecordResult "${testName}" "failed" "Image not pullable from Quay after push" "${elapsed}" | ||
| return 1 | ||
| fi | ||
|
|
||
| elapsed=$(( $(date +%s) - start )) | ||
| RecordResult "${testName}" "passed" "" "${elapsed}" | ||
| return 0 | ||
| } | ||
|
|
||
| ################################################################################ | ||
| # Test Case 2: Verify ODF PVC backing Quay storage | ||
| ################################################################################ | ||
| function RunOdfPvcCheck () { | ||
| typeset testName="[sig-interop][Jira:INTEROP][Feature:Quay] Verify ODF PVC backing Quay storage" | ||
| typeset -i start elapsed | ||
| start=$(date +%s) | ||
|
|
||
| typeset pvcCount | ||
| pvcCount=$(oc get pvc -n "${QUAY_NS}" -l app=quay -o json 2>/dev/null | python3 -c " | ||
| import sys, json | ||
| data = json.load(sys.stdin) | ||
| items = data.get('items', []) | ||
| print(len(items)) | ||
| " 2>/dev/null || echo "0") | ||
|
|
||
| if [[ "${pvcCount}" == "0" ]]; then | ||
| pvcCount=$(oc get pvc -n "${QUAY_NS}" -o json | python3 -c " | ||
| import sys, json | ||
| data = json.load(sys.stdin) | ||
| items = [i for i in data.get('items', []) if 'quay' in i['metadata'].get('name','').lower()] | ||
| print(len(items)) | ||
| " 2>/dev/null || echo "0") | ||
| fi | ||
|
|
||
| if [[ "${pvcCount}" == "0" ]]; then | ||
| elapsed=$(( $(date +%s) - start )) | ||
| RecordResult "${testName}" "failed" "No Quay-related PVCs found in ${QUAY_NS}" "${elapsed}" | ||
| return 1 | ||
| fi | ||
|
|
||
| typeset unboundPvcs | ||
| unboundPvcs=$(oc get pvc -n "${QUAY_NS}" -o json | python3 -c " | ||
| import sys, json | ||
| data = json.load(sys.stdin) | ||
| items = [i for i in data.get('items', []) if 'quay' in i['metadata'].get('name','').lower()] | ||
| unbound = [i['metadata']['name'] for i in items if i['status'].get('phase') != 'Bound'] | ||
| print(' '.join(unbound)) | ||
| " 2>/dev/null || echo "") | ||
|
|
||
| if [[ -n "${unboundPvcs}" ]]; then | ||
| elapsed=$(( $(date +%s) - start )) | ||
| RecordResult "${testName}" "failed" "Unbound PVCs: ${unboundPvcs}" "${elapsed}" | ||
| return 1 | ||
| fi | ||
|
|
||
| typeset odfBacked | ||
| odfBacked=$(oc get pvc -n "${QUAY_NS}" -o json | python3 -c " | ||
| import sys, json | ||
| data = json.load(sys.stdin) | ||
| items = [i for i in data.get('items', []) if 'quay' in i['metadata'].get('name','').lower()] | ||
| sc_names = set(i['spec'].get('storageClassName','') for i in items) | ||
| odf = any('ocs' in s or 'ceph' in s or 'odf' in s for s in sc_names) | ||
| print('true' if odf else 'false') | ||
| " 2>/dev/null || echo "false") | ||
|
Comment on lines
+229
to
+237
Contributor
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. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Validate the storage backend instead of the StorageClass name. A StorageClass name containing 🤖 Prompt for AI Agents
Comment on lines
+214
to
+237
Contributor
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. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Use one Quay PVC selection for all checks. Lines 195-200 count PVCs with 🤖 Prompt for AI Agents |
||
|
|
||
| if [[ "${odfBacked}" != "true" ]]; then | ||
| elapsed=$(( $(date +%s) - start )) | ||
| RecordResult "${testName}" "failed" "Quay PVCs not using ODF/Ceph storage class" "${elapsed}" | ||
| return 1 | ||
| fi | ||
|
|
||
| elapsed=$(( $(date +%s) - start )) | ||
| RecordResult "${testName}" "passed" "" "${elapsed}" | ||
| return 0 | ||
| } | ||
|
|
||
| ################################################################################ | ||
| # Test Case 3: ACS scan of pushed Quay image | ||
| ################################################################################ | ||
| function RunAcsScan () { | ||
| typeset testName="[sig-interop][Jira:INTEROP][Feature:Quay] ACS scan of pushed Quay image" | ||
| typeset -i start elapsed | ||
| start=$(date +%s) | ||
|
|
||
| typeset acsHost acsPassword | ||
| acsHost=$(oc get route -n stackrox central -o jsonpath='{.spec.host}' 2>/dev/null || echo "") | ||
| if [[ -z "${acsHost}" ]]; then | ||
| elapsed=$(( $(date +%s) - start )) | ||
| RecordResult "${testName}" "failed" "ACS Central route not found" "${elapsed}" | ||
| return 1 | ||
| fi | ||
|
|
||
| acsPassword=$(oc get secret -n stackrox central-htpasswd -o jsonpath='{.data.password}' 2>/dev/null | base64 -d || echo "") | ||
| if [[ -z "${acsPassword}" ]]; then | ||
| elapsed=$(( $(date +%s) - start )) | ||
| RecordResult "${testName}" "failed" "ACS admin password not found" "${elapsed}" | ||
| return 1 | ||
| fi | ||
|
|
||
| typeset pushTarget="${QUAY_HOST}/interop-smoke-test/ubi-smoke:${imageTag}" | ||
| typeset -i attempts=0 maxAttempts=20 | ||
|
|
||
| while (( attempts < maxAttempts )); do | ||
| typeset scanResult | ||
| scanResult=$(curl -sk -u "admin:${acsPassword}" \ | ||
| "https://${acsHost}/v1/images?query=Image:${pushTarget}" 2>/dev/null || echo "") | ||
|
|
||
| if echo "${scanResult}" | python3 -c " | ||
| import sys, json | ||
| data = json.load(sys.stdin) | ||
| images = data.get('images', []) | ||
| sys.exit(0 if len(images) > 0 else 1) | ||
| " 2>/dev/null; then | ||
| elapsed=$(( $(date +%s) - start )) | ||
| RecordResult "${testName}" "passed" "" "${elapsed}" | ||
| return 0 | ||
| fi | ||
|
|
||
| attempts=$((attempts + 1)) | ||
| sleep 15 | ||
| done | ||
|
|
||
| elapsed=$(( $(date +%s) - start )) | ||
| RecordResult "${testName}" "failed" "ACS did not detect pushed image within 5 minutes" "${elapsed}" | ||
| return 1 | ||
| } | ||
|
|
||
| ################################################################################ | ||
| # Main execution | ||
| ################################################################################ | ||
|
|
||
| function Main () { | ||
| DiscoverQuay | ||
| GetQuayAuth | ||
| PreflightCheck || { echo "FATAL: Quay not reachable; skipping all tests" >&2; exit 1; } | ||
| CreateTestOrg | ||
|
|
||
| typeset -i status=0 | ||
| RunPushPull || status=1 | ||
| RunOdfPvcCheck || status=1 | ||
| RunAcsScan || status=1 | ||
|
|
||
| rm -f /tmp/quay-auth.json | ||
|
|
||
| if [[ "${MAP_TESTS}" == "true" ]]; then | ||
| eval "$( | ||
| typeset -a _fURL=() | ||
| type -t wget 1>/dev/null && _fURL=(wget --timeout=30 -qO-) || _fURL=(curl --connect-timeout 10 --max-time 30 -fsSL) | ||
| "${_fURL[@]}" \ | ||
| https://raw.githubusercontent.com/RedHatQE/OpenShift-LP-QE--Tools/refs/heads/main/libs/bash/ci-operator/interop/common/ExitTrap--PostProcessPrep.sh | ||
| )" || true | ||
| if type -t ExitTrap--PostProcessPrep 1>/dev/null; then | ||
| LP_IO__ET_PPP__NEW_TS_NAME="${DR__RP__CR_COMP_NAME}--%s" \ | ||
| ExitTrap--PostProcessPrep || true | ||
| fi | ||
| fi | ||
|
|
||
| exit "${status}" | ||
| } | ||
|
|
||
| Main "$@" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "path": "interop-tests/opp-quay-smoke/interop-tests-opp-quay-smoke-ref.yaml", | ||
| "owners": { | ||
| "approvers": [ | ||
| "cspi-qe-ocp-lp" | ||
| ], | ||
| "reviewers": [ | ||
| "cspi-qe-ocp-lp" | ||
| ] | ||
| } | ||
| } |
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.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Verify TLS before sending Quay credentials.
These authenticated curl and skopeo calls disable TLS verification. An interceptor for the Quay route can obtain the Quay password, Basic authorization value, Bearer token, or registry credentials. Configure the route CA with
--cacert, then remove-k,--dest-tls-verify=false, and--tls-verify=false.Also applies to: 163-174
🧰 Tools
🪛 ast-grep (0.45.1)
[warning] 128-129: curl is invoked with -k/--insecure, which disables TLS certificate verification and exposes the connection to man-in-the-middle attacks. Remove the insecure flag and let curl validate the server certificate; if you need to trust a private CA, pin it with --cacert instead.$(echo -n "$ {QUAY_USER}:${QUAY_PASSWORD}" | base64)"
Context: curl -sk -H "Authorization: Basic
"https://${QUAY_HOST}/api/v1/user/"
Note: [CWE-295] Improper Certificate Validation.
(curl-insecure-tls-bash)
[warning] 136-139: curl is invoked with -k/--insecure, which disables TLS certificate verification and exposes the connection to man-in-the-middle attacks. Remove the insecure flag and let curl validate the server certificate; if you need to trust a private CA, pin it with --cacert instead.
Context: curl -sk -X POST "https://${QUAY_HOST}/api/v1/organization/"
-H "Authorization: Bearer ${QUAY_TOKEN}"
-H "Content-Type: application/json"
-d '{"name":"interop-smoke-test","email":"interop-test@example.com"}'
Note: [CWE-295] Improper Certificate Validation.
(curl-insecure-tls-bash)
🤖 Prompt for AI Agents
Source: Linters/SAST tools