Skip to content

Commit 2624915

Browse files
committed
test(RELEASE-2331): multi-arch manifest list for push-to-addons-registry
Adds end-to-end coverage for multi-arch (amd64 + arm64) operator index flows for the push-to-addons-registry pipeline. Signed-off-by: Elena German <elgerman@redhat.com> Assisted-by: Claude
1 parent fda9f64 commit 2624915

4 files changed

Lines changed: 293 additions & 16 deletions

File tree

integration-tests/push-to-addons-registry/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
This test validates the addon registry push pipeline functionality.
44

5+
The Konflux build is configured for a **multi-arch manifest list** (amd64 and arm64), matching common OSD addon index images.
6+
**Note:** This test patches the component’s PaC templates in the PR *before merge* to ensure the `build-platforms` param
7+
includes `linux/amd64` and `linux/arm64`, so the push pipeline produces a multi-arch index for verification.
58
## Test-Specific Configuration
69

710
### Files Structure

integration-tests/push-to-addons-registry/resources/tenant/component.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ metadata:
66
git-provider: github
77
build.appstudio.openshift.io/request: configure-pac
88
image.redhat.com/generate: '{"visibility": "public"}'
9+
build.appstudio.openshift.io/pipeline: '{"name": "docker-build-multi-platform-oci-ta", "bundle": "latest"}'
910
name: ${component_name}
1011
labels:
1112
originating-tool: "${originating_tool}"

integration-tests/push-to-addons-registry/test.sh

Lines changed: 195 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# --- Global Script Variables (Defaults) ---
22
CLEANUP="true"
33
NO_CVE="true" # Default to true
4+
# GitHub API curl retries (override in CI/local: export GITHUB_API_CURL_RETRY=5)
5+
GITHUB_API_CURL_RETRY="${GITHUB_API_CURL_RETRY:-3}"
46

57
# Variables that will be set by functions and used globally:
68
# component_branch, component_base_branch, component_repo_name (from test.env or similar)
@@ -15,6 +17,140 @@ NO_CVE="true" # Default to true
1517
# component_push_plr_name (set by wait_for_plr_to_appear)
1618
# RELEASE_NAME, RELEASE_NAMESPACE (set and exported by wait_for_release)
1719

20+
# GET a GitHub API JSON resource; prints the response body on success, returns 1 on curl/HTTP error.
21+
# Args: token url detail_msg fallback_msg (used when curl fails, with/without .message from body)
22+
github_api_get_json() {
23+
local token="$1"
24+
local url="$2"
25+
local detail_msg="$3"
26+
local fallback_msg="$4"
27+
local response api_msg
28+
local xtrace_was_on=0
29+
case $- in
30+
*x*) xtrace_was_on=1 ;;
31+
esac
32+
if [ "${xtrace_was_on}" -eq 1 ]; then
33+
set +x
34+
fi
35+
36+
if ! response=$(curl --retry "${GITHUB_API_CURL_RETRY}" -s --fail-with-body \
37+
-H "Authorization: token ${token}" \
38+
"${url}"); then
39+
if [ "${xtrace_was_on}" -eq 1 ]; then
40+
set -x
41+
fi
42+
if api_msg=$(jq -r '.message // empty' <<< "${response}" 2>/dev/null) && [ -n "${api_msg}" ]; then
43+
echo "❌ error: ${detail_msg}: ${api_msg}" >&2
44+
else
45+
echo "❌ error: ${fallback_msg}" >&2
46+
fi
47+
return 1
48+
fi
49+
if [ "${xtrace_was_on}" -eq 1 ]; then
50+
set -x
51+
fi
52+
echo "${response}"
53+
}
54+
55+
patch_component_source_before_merge() {
56+
# CI often runs scripts under xtrace (bash -x). Disable tracing only while handling tokens.
57+
local xtrace_was_on=0
58+
case $- in
59+
*x*) xtrace_was_on=1 ;;
60+
esac
61+
if [ "${xtrace_was_on}" -eq 1 ]; then
62+
set +x
63+
fi
64+
65+
trap 'unset secret_value 2>/dev/null; if [ "${xtrace_was_on}" -eq 1 ]; then set -x; fi' RETURN
66+
67+
echo "Patching component source BEFORE MERGE to ensure multi-arch build..."
68+
69+
# PaC GitHub token: do not export — scoped env only for the helper that requires GH_TOKEN.
70+
# Vault file uses ${component_name} placeholders; envsubst runs at kubectl apply, not on disk.
71+
local secret_value
72+
secret_value=$(yq '. | select(.metadata.name | contains("pipelines-as-code-secret-")) | .stringData.password' \
73+
"${SUITE_DIR}/resources/tenant/secrets/tenant-secrets.yaml" | head -n 1)
74+
while [[ "${secret_value}" == *$'\n' ]]; do
75+
secret_value="${secret_value%$'\n'}"
76+
done
77+
78+
if [ -z "${secret_value}" ] || [ "${secret_value}" = "null" ]; then
79+
log_error "PaC token not found in tenant secrets (pipelines-as-code-secret-*)"
80+
fi
81+
82+
local file_names=(
83+
".tekton/${component_name}-pull-request.yaml"
84+
".tekton/${component_name}-push.yaml"
85+
)
86+
local head_sha pr_response
87+
pr_response=$(github_api_get_json "${secret_value}" \
88+
"https://api.github.com/repos/${component_repo_name}/pulls/${pr_number}" \
89+
"GitHub API error fetching PR ${pr_number}" \
90+
"failed to fetch PR ${pr_number} from ${component_repo_name} (check PaC token and repo access)") || exit 1
91+
head_sha=$(jq -r -e '.head.sha' <<< "${pr_response}") || {
92+
log_error "missing or invalid .head.sha in PR ${pr_number} response"
93+
}
94+
95+
for file_name in "${file_names[@]}"; do
96+
local decoded_contents encoded_contents
97+
local contents_response encoded_content_field
98+
echo "Patching ${file_name}..."
99+
100+
contents_response=$(github_api_get_json "${secret_value}" \
101+
"https://api.github.com/repos/${component_repo_name}/contents/${file_name}?ref=${head_sha}" \
102+
"GitHub API error fetching ${file_name}" \
103+
"failed to fetch ${file_name} at ref ${head_sha} from ${component_repo_name}") || exit 1
104+
105+
encoded_content_field=$(jq -r -e '.content' <<< "${contents_response}") || {
106+
log_error "missing or invalid .content for ${file_name} in PR ${pr_number}"
107+
}
108+
if ! decoded_contents=$(base64 -d <<< "${encoded_content_field}"); then
109+
log_error "failed to base64-decode ${file_name} from PR ${pr_number}"
110+
fi
111+
if [ -z "${decoded_contents}" ]; then
112+
log_error "decoded contents for ${file_name} are empty"
113+
fi
114+
115+
encoded_contents=$(
116+
set -eo pipefail
117+
work_dir=$(mktemp -d)
118+
trap 'rm -rf "${work_dir}"' EXIT
119+
nopath_file_name=$(basename "${file_name}")
120+
echo "${decoded_contents}" > "${work_dir}/${nopath_file_name}"
121+
122+
# Ensure linux/amd64 + linux/arm64 are present.
123+
if yq -e '(.spec.params[]? | select(.name == "build-platforms") | .value | type) == "!!seq"' \
124+
"${work_dir}/${nopath_file_name}" >/dev/null 2>&1; then
125+
yq -i '(.spec.params[] | select(.name == "build-platforms") | .value) |= ([.[] | select(. != "linux/arm64")] + ["linux/arm64"])' \
126+
"${work_dir}/${nopath_file_name}"
127+
yq -i '(.spec.params[] | select(.name == "build-platforms") | .value) |= ([.[] | select(. != "linux/amd64")] + ["linux/amd64"])' \
128+
"${work_dir}/${nopath_file_name}"
129+
elif yq -e '(.spec.params[]? | select(.name == "build-platforms"))' \
130+
"${work_dir}/${nopath_file_name}" >/dev/null 2>&1; then
131+
yq -i '(.spec.params[] | select(.name == "build-platforms") | .value) = ["linux/amd64", "linux/arm64"]' \
132+
"${work_dir}/${nopath_file_name}"
133+
else
134+
yq -i '.spec.params += [{"name": "build-platforms", "value": ["linux/amd64", "linux/arm64"]}]' \
135+
"${work_dir}/${nopath_file_name}"
136+
fi
137+
138+
base64 -w 0 < "${work_dir}/${nopath_file_name}"
139+
) || {
140+
log_error "failed to patch ${file_name} for multi-arch build"
141+
}
142+
143+
GH_TOKEN="${secret_value}" "${SCRIPT_DIR}/scripts/update-file-in-pull-request.sh" \
144+
"${component_repo_name}" \
145+
"${pr_number}" \
146+
"${file_name}" \
147+
"Update PaC templates for multi-arch build" \
148+
"${encoded_contents}" || log_error "failed to update ${file_name} in PR ${pr_number}"
149+
done
150+
151+
echo "✅️ Successfully patched component PaC templates for multi-arch."
152+
}
153+
18154
# Function to verify Release contents
19155
# Relies on global variables: RELEASE_NAMES, RELEASE_NAMESPACE, SCRIPT_DIR, managed_namespace, managed_sa_name, NO_CVE
20156
verify_release_contents() {
@@ -29,13 +165,34 @@ verify_release_contents() {
29165
log_error "Could not retrieve Release JSON for ${RELEASE_NAME}"
30166
fi
31167

32-
echo "Release JSON: ${release_json}"
33-
34168
local failures=0
35-
local image_url mergerequest_url
169+
local image_url mergerequest_url image_arches image_shasum released_status
170+
171+
image_url=$(jq -r '.status.artifacts.images[0]?.urls[0]? // ""' <<< "${release_json}")
172+
mergerequest_url=$(jq -r '.status.artifacts.merge_requests[0]?.url? // ""' <<< "${release_json}")
173+
# Release may list one arch per manifest/index entry (e.g. duplicate amd64); compare distinct sets.
174+
# Strip optional linux/ prefix (e.g. linux/amd64 -> amd64). Default null/missing .arches to [].
175+
image_arches=$(jq -r '(.status.artifacts.images[0]?.arches? // [])
176+
| map((tostring | split("/") | .[-1]))
177+
| unique
178+
| join(" ")' <<< "${release_json}")
179+
image_shasum=$(jq -r '.status.artifacts.images[0]?.shasum? // ""' <<< "${release_json}")
180+
released_status=$(jq -r '([.status.conditions[]? | select(.type=="Released") | .status] | first) // ""' <<< "${release_json}")
36181

37-
image_url=$(jq -r '.status.artifacts.images[0].urls[0] // ""' <<< "${release_json}")
38-
mergerequest_url=$(jq -r '.status.artifacts.merge_requests[0].url // ""' <<< "${release_json}")
182+
echo "Release fields under validation:"
183+
echo " Released: ${released_status}"
184+
echo " image_url: ${image_url}"
185+
echo " mergerequest_url: ${mergerequest_url}"
186+
echo " image_arches: ${image_arches}"
187+
echo " image_shasum: ${image_shasum}"
188+
189+
echo "Checking Released=True..."
190+
if [ "${released_status}" = "True" ]; then
191+
echo "✅️ Released=True"
192+
else
193+
echo "🔴 Released was not True (found: '${released_status}')"
194+
failures=$((failures+1))
195+
fi
39196

40197
echo "Checking image_url..."
41198
if [ -n "${image_url}" ]; then
@@ -52,6 +209,39 @@ verify_release_contents() {
52209
failures=$((failures+1))
53210
fi
54211

212+
echo "Checking image arches include amd64 and arm64..."
213+
if [[ " ${image_arches} " == *" amd64 "* && " ${image_arches} " == *" arm64 "* ]]; then
214+
echo "✅️ Found required arches: ${image_arches}"
215+
else
216+
echo "🔴 Missing required arches (need: amd64 and arm64), found: '${image_arches}'"
217+
failures=$((failures+1))
218+
fi
219+
220+
echo "Checking image shasum (manifest list digest) is present..."
221+
if [[ "${image_shasum}" == sha256:* ]]; then
222+
echo "✅️ image_shasum: ${image_shasum}"
223+
else
224+
echo "🔴 image_shasum missing or invalid: '${image_shasum}'"
225+
failures=$((failures+1))
226+
fi
227+
228+
echo "Checking skopeo inspect succeeds for both arches (digest pull + registry auth)..."
229+
if [ -n "${image_url}" ] && [[ "${image_shasum}" == sha256:* ]]; then
230+
set +e
231+
"${SCRIPT_DIR}/scripts/skopeo-verify-image.sh" \
232+
"${image_url}" "${image_shasum}" \
233+
"${SUITE_DIR}/resources/managed/secrets/managed-secrets.yaml" \
234+
"amd64 arm64"
235+
skopeo_rc=$?
236+
set -e
237+
if [ "${skopeo_rc}" -ne 0 ]; then
238+
failures=$((failures+1))
239+
fi
240+
elif [ -n "${image_url}" ]; then
241+
echo "🔴 Skipping skopeo multi-arch check: image_shasum missing or not sha256:*"
242+
failures=$((failures+1))
243+
fi
244+
55245
if [ "${failures}" -gt 0 ]; then
56246
echo "🔴 Test has FAILED with ${failures} failure(s)!"
57247
failed_releases="${RELEASE_NAME} ${failed_releases}"
Lines changed: 94 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,128 @@
11
#!/usr/bin/env bash
22
# Verifies that an image is pullable from its target registry using skopeo.
33
#
4-
# Usage: skopeo-verify-image.sh <image_url> <image_shasum> <managed_secrets_yaml>
4+
# Usage: skopeo-verify-image.sh <image_url> <image_shasum> <managed_secrets_yaml> [arches]
55
#
66
# Arguments:
77
# image_url - The published image URL (may include a tag or digest)
88
# image_shasum - The image digest (e.g. sha256:abc123...)
99
# managed_secrets_yaml - Path to the decrypted managed-secrets.yaml file
10+
# arches (optional) - Space-separated list of GOARCH values (e.g. "amd64 arm64").
11+
# When set, verifies the digest is an OCI image index or Docker
12+
# manifest list (--raw mediaType), then runs skopeo inspect with
13+
# --override-arch for each arch. On failure, re-runs the failing
14+
# inspect without suppressing stderr so logs stay debuggable.
15+
#
16+
# Environment:
17+
# SKOPEO_RETRY_TIMES - skopeo --retry-times value (default: 3). Override in CI/local:
18+
# export SKOPEO_RETRY_TIMES=5
1019
#
1120
# Exits with 0 on success, 1 on failure.
1221

1322
set -euo pipefail
1423

24+
SKOPEO_RETRY_TIMES="${SKOPEO_RETRY_TIMES:-3}"
25+
1526
image_url="${1:?image_url argument is required}"
1627
image_shasum="${2:?image_shasum argument is required}"
1728
managed_secrets_yaml="${3:?managed_secrets_yaml argument is required}"
29+
optional_arches="${4:-}"
1830

19-
if [[ "${image_url}" == *"@"* ]]; then
20-
STRIPPED_PULLSPEC="${image_url%@*}"
31+
STRIPPED_PULLSPEC="${image_url}"
32+
if [[ "${STRIPPED_PULLSPEC}" == *"@"* ]]; then
33+
STRIPPED_PULLSPEC="${STRIPPED_PULLSPEC%@*}"
2134
echo "Stripped digest from: ${image_url} -> ${STRIPPED_PULLSPEC}"
22-
elif [[ "${image_url}" == *":"* ]]; then
23-
STRIPPED_PULLSPEC="${image_url%:*}"
24-
echo "Stripped tag from: ${image_url} -> ${STRIPPED_PULLSPEC}"
35+
fi
36+
37+
# Tags appear only after the last '/'; registry ports (host:port) must not be stripped.
38+
if [[ "${STRIPPED_PULLSPEC}" == */* ]]; then
39+
path_prefix="${STRIPPED_PULLSPEC%/*}"
40+
final_segment="${STRIPPED_PULLSPEC##*/}"
2541
else
26-
STRIPPED_PULLSPEC="${image_url}"
42+
path_prefix=""
43+
final_segment="${STRIPPED_PULLSPEC}"
44+
fi
45+
if [[ "${final_segment}" == *":"* ]]; then
46+
final_segment="${final_segment%:*}"
47+
if [[ -n "${path_prefix}" ]]; then
48+
STRIPPED_PULLSPEC="${path_prefix}/${final_segment}"
49+
else
50+
STRIPPED_PULLSPEC="${final_segment}"
51+
fi
52+
echo "Stripped tag from: ${image_url} -> ${STRIPPED_PULLSPEC}"
53+
elif [[ "${STRIPPED_PULLSPEC}" == "${image_url}" ]]; then
2754
echo "No tag or digest found, using original as is: ${STRIPPED_PULLSPEC}"
2855
fi
2956

3057
COMPLETE_PULLSPEC="${STRIPPED_PULLSPEC}@${image_shasum}"
3158
echo "New complete pullspec: ${COMPLETE_PULLSPEC}"
3259

60+
# CI often runs scripts under xtrace (bash -x). Disable tracing while loading registry credentials.
61+
xtrace_was_on=0
62+
case $- in
63+
*x*) xtrace_was_on=1 ;;
64+
esac
65+
if [ "${xtrace_was_on}" -eq 1 ]; then
66+
set +x
67+
fi
68+
3369
DOCKER_CONFIG="$(mktemp -d)"
3470
export DOCKER_CONFIG
71+
cleanup_skopeo_verify() {
72+
if [ "${xtrace_was_on}" -eq 1 ]; then
73+
set -x
74+
fi
75+
rm -rf "${DOCKER_CONFIG}"
76+
}
77+
trap cleanup_skopeo_verify EXIT
78+
79+
dockerconfig_b64="$(
80+
yq '. | select(.metadata.name | contains("push-")) | .data.".dockerconfigjson"' \
81+
"${managed_secrets_yaml}" | head -n 1
82+
)"
83+
if [[ -z "${dockerconfig_b64}" || "${dockerconfig_b64}" == "null" ]]; then
84+
echo "🔴 Failed to find push-* dockerconfigjson in managed secrets."
85+
exit 1
86+
fi
87+
printf '%s' "${dockerconfig_b64}" | base64 -d > "${DOCKER_CONFIG}/config.json"
88+
unset dockerconfig_b64
89+
90+
if [[ -n "${optional_arches}" ]]; then
91+
set +e
92+
raw_manifest="$(skopeo inspect --tls-verify=true --retry-times "${SKOPEO_RETRY_TIMES}" --raw "docker://${COMPLETE_PULLSPEC}" 2>/dev/null)"
93+
rc=$?
94+
set -e
95+
if [[ "${rc}" -ne 0 ]]; then
96+
echo "🔴 Failed to fetch raw manifest for '${COMPLETE_PULLSPEC}'."
97+
skopeo inspect --tls-verify=true --retry-times "${SKOPEO_RETRY_TIMES}" --raw "docker://${COMPLETE_PULLSPEC}"
98+
exit 1
99+
fi
35100

36-
yq '. | select(.metadata.name | contains("push-")) | .data.".dockerconfigjson"' \
37-
"${managed_secrets_yaml}" | base64 -d > "${DOCKER_CONFIG}/config.json"
101+
if ! media_type="$(jq -r '.mediaType // empty' <<< "${raw_manifest}")" || [[ -z "${media_type}" ]]; then
102+
echo "🔴 Unable to determine manifest mediaType from raw inspect output."
103+
echo "Raw manifest (first 200 chars): $(head -c 200 <<< "${raw_manifest}")"
104+
exit 1
105+
fi
106+
if [[ "${media_type}" != "application/vnd.oci.image.index.v1+json" ]] &&
107+
[[ "${media_type}" != "application/vnd.docker.distribution.manifest.list.v2+json" ]]; then
108+
echo "🔴 Expected OCI image index or Docker manifest list, found mediaType: '${media_type}'"
109+
exit 1
110+
fi
111+
echo "✅️ Verified multi-arch index (mediaType: ${media_type})."
38112

39-
if skopeo inspect --tls-verify=true "docker://${COMPLETE_PULLSPEC}" &>/dev/null; then
113+
for arch in ${optional_arches}; do
114+
if skopeo inspect --tls-verify=true --retry-times "${SKOPEO_RETRY_TIMES}" --override-arch "${arch}" "docker://${COMPLETE_PULLSPEC}" &>/dev/null; then
115+
echo "✅️ Image '${COMPLETE_PULLSPEC}' can be inspected for arch ${arch}."
116+
else
117+
echo "🔴 Failed skopeo inspect for arch ${arch} on '${COMPLETE_PULLSPEC}'."
118+
skopeo inspect --tls-verify=true --retry-times "${SKOPEO_RETRY_TIMES}" --override-arch "${arch}" "docker://${COMPLETE_PULLSPEC}"
119+
exit 1
120+
fi
121+
done
122+
elif skopeo inspect --tls-verify=true --retry-times "${SKOPEO_RETRY_TIMES}" "docker://${COMPLETE_PULLSPEC}" &>/dev/null; then
40123
echo "✅️ Image '${COMPLETE_PULLSPEC}' can be pulled using skopeo."
41124
else
42125
echo "🔴 Failed to pull or inspect image '${COMPLETE_PULLSPEC}'."
43-
skopeo inspect --tls-verify=true "docker://${COMPLETE_PULLSPEC}"
126+
skopeo inspect --tls-verify=true --retry-times "${SKOPEO_RETRY_TIMES}" "docker://${COMPLETE_PULLSPEC}"
44127
exit 1
45128
fi

0 commit comments

Comments
 (0)