Skip to content

Commit 1b2c40f

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 5857a34 commit 1b2c40f

4 files changed

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

32-
echo "Release JSON: ${release_json}"
33-
34138
local failures=0
35-
local image_url mergerequest_url
139+
local image_url mergerequest_url image_arches image_shasum released_status
36140

37141
image_url=$(jq -r '.status.artifacts.images[0].urls[0] // ""' <<< "${release_json}")
38142
mergerequest_url=$(jq -r '.status.artifacts.merge_requests[0].url // ""' <<< "${release_json}")
143+
# Release may list one arch per manifest/index entry (e.g. duplicate amd64); compare distinct sets.
144+
# Strip optional linux/ prefix (e.g. linux/amd64 -> amd64). Default null/missing .arches to [].
145+
image_arches=$(jq -r '(.status.artifacts.images[0].arches // [])
146+
| map((tostring | split("/") | .[-1]))
147+
| unique
148+
| join(" ")' <<< "${release_json}")
149+
image_shasum=$(jq -r '.status.artifacts.images[0].shasum // ""' <<< "${release_json}")
150+
released_status=$(jq -r '.status.conditions[]? | select(.type=="Released") | .status // ""' <<< "${release_json}")
151+
152+
echo "Release fields under validation:"
153+
echo " Released: ${released_status}"
154+
echo " image_url: ${image_url}"
155+
echo " mergerequest_url: ${mergerequest_url}"
156+
echo " image_arches: ${image_arches}"
157+
echo " image_shasum: ${image_shasum}"
158+
159+
echo "Checking Released=True..."
160+
if [ "${released_status}" = "True" ]; then
161+
echo "✅️ Released=True"
162+
else
163+
echo "🔴 Released was not True (found: '${released_status}')"
164+
failures=$((failures+1))
165+
fi
39166

40167
echo "Checking image_url..."
41168
if [ -n "${image_url}" ]; then
@@ -52,6 +179,39 @@ verify_release_contents() {
52179
failures=$((failures+1))
53180
fi
54181

182+
echo "Checking image arches include amd64 and arm64..."
183+
if [[ " ${image_arches} " == *" amd64 "* && " ${image_arches} " == *" arm64 "* ]]; then
184+
echo "✅️ Found required arches: ${image_arches}"
185+
else
186+
echo "🔴 Missing required arches (need: amd64 and arm64), found: '${image_arches}'"
187+
failures=$((failures+1))
188+
fi
189+
190+
echo "Checking image shasum (manifest list digest) is present..."
191+
if [[ "${image_shasum}" == sha256:* ]]; then
192+
echo "✅️ image_shasum: ${image_shasum}"
193+
else
194+
echo "🔴 image_shasum missing or invalid: '${image_shasum}'"
195+
failures=$((failures+1))
196+
fi
197+
198+
echo "Checking skopeo inspect succeeds for both arches (digest pull + registry auth)..."
199+
if [ -n "${image_url}" ] && [[ "${image_shasum}" == sha256:* ]]; then
200+
set +e
201+
"${SCRIPT_DIR}/scripts/skopeo-verify-image.sh" \
202+
"${image_url}" "${image_shasum}" \
203+
"${SUITE_DIR}/resources/managed/secrets/managed-secrets.yaml" \
204+
"amd64 arm64"
205+
skopeo_rc=$?
206+
set -e
207+
if [ "${skopeo_rc}" -ne 0 ]; then
208+
failures=$((failures+1))
209+
fi
210+
elif [ -n "${image_url}" ]; then
211+
echo "🔴 Skipping skopeo multi-arch check: image_shasum missing or not sha256:*"
212+
failures=$((failures+1))
213+
fi
214+
55215
if [ "${failures}" -gt 0 ]; then
56216
echo "🔴 Test has FAILED with ${failures} failure(s)!"
57217
failed_releases="${RELEASE_NAME} ${failed_releases}"

integration-tests/scripts/skopeo-verify-image.sh

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
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.
1015
#
1116
# Exits with 0 on success, 1 on failure.
1217

@@ -15,28 +20,93 @@ set -euo pipefail
1520
image_url="${1:?image_url argument is required}"
1621
image_shasum="${2:?image_shasum argument is required}"
1722
managed_secrets_yaml="${3:?managed_secrets_yaml argument is required}"
23+
optional_arches="${4:-}"
1824

19-
if [[ "${image_url}" == *"@"* ]]; then
20-
STRIPPED_PULLSPEC="${image_url%@*}"
25+
STRIPPED_PULLSPEC="${image_url}"
26+
if [[ "${STRIPPED_PULLSPEC}" == *"@"* ]]; then
27+
STRIPPED_PULLSPEC="${STRIPPED_PULLSPEC%@*}"
2128
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}"
29+
fi
30+
31+
# Tags appear only after the last '/'; registry ports (host:port) must not be stripped.
32+
if [[ "${STRIPPED_PULLSPEC}" == */* ]]; then
33+
path_prefix="${STRIPPED_PULLSPEC%/*}"
34+
final_segment="${STRIPPED_PULLSPEC##*/}"
2535
else
26-
STRIPPED_PULLSPEC="${image_url}"
36+
path_prefix=""
37+
final_segment="${STRIPPED_PULLSPEC}"
38+
fi
39+
if [[ "${final_segment}" == *":"* ]]; then
40+
final_segment="${final_segment%:*}"
41+
if [[ -n "${path_prefix}" ]]; then
42+
STRIPPED_PULLSPEC="${path_prefix}/${final_segment}"
43+
else
44+
STRIPPED_PULLSPEC="${final_segment}"
45+
fi
46+
echo "Stripped tag from: ${image_url} -> ${STRIPPED_PULLSPEC}"
47+
elif [[ "${STRIPPED_PULLSPEC}" == "${image_url}" ]]; then
2748
echo "No tag or digest found, using original as is: ${STRIPPED_PULLSPEC}"
2849
fi
2950

3051
COMPLETE_PULLSPEC="${STRIPPED_PULLSPEC}@${image_shasum}"
3152
echo "New complete pullspec: ${COMPLETE_PULLSPEC}"
3253

54+
# CI often runs scripts under xtrace (bash -x). Disable tracing while loading registry credentials.
55+
xtrace_was_on=0
56+
case $- in
57+
*x*) xtrace_was_on=1 ;;
58+
esac
59+
if [ "${xtrace_was_on}" -eq 1 ]; then
60+
set +x
61+
fi
62+
3363
DOCKER_CONFIG="$(mktemp -d)"
3464
export DOCKER_CONFIG
65+
cleanup_skopeo_verify() {
66+
if [ "${xtrace_was_on}" -eq 1 ]; then
67+
set -x
68+
fi
69+
rm -rf "${DOCKER_CONFIG}"
70+
}
71+
trap cleanup_skopeo_verify EXIT
72+
73+
dockerconfig_b64="$(
74+
yq '. | select(.metadata.name | contains("push-")) | .data.".dockerconfigjson"' \
75+
"${managed_secrets_yaml}" | head -n 1
76+
)"
77+
if [[ -z "${dockerconfig_b64}" || "${dockerconfig_b64}" == "null" ]]; then
78+
echo "🔴 Failed to find push-* dockerconfigjson in managed secrets."
79+
exit 1
80+
fi
81+
printf '%s' "${dockerconfig_b64}" | base64 -d > "${DOCKER_CONFIG}/config.json"
82+
unset dockerconfig_b64
3583

36-
yq '. | select(.metadata.name | contains("push-")) | .data.".dockerconfigjson"' \
37-
"${managed_secrets_yaml}" | base64 -d > "${DOCKER_CONFIG}/config.json"
84+
if [[ -n "${optional_arches}" ]]; then
85+
raw_manifest="$(
86+
skopeo inspect --tls-verify=true --retry-times 3 --raw "docker://${COMPLETE_PULLSPEC}" 2>/dev/null
87+
)"
88+
if ! media_type="$(jq -r '.mediaType // empty' <<< "${raw_manifest}")" || [[ -z "${media_type}" ]]; then
89+
echo "🔴 Unable to determine manifest mediaType from raw inspect output."
90+
echo "Raw manifest (first 200 chars): $(head -c 200 <<< "${raw_manifest}")"
91+
exit 1
92+
fi
93+
if [[ "${media_type}" != "application/vnd.oci.image.index.v1+json" ]] &&
94+
[[ "${media_type}" != "application/vnd.docker.distribution.manifest.list.v2+json" ]]; then
95+
echo "🔴 Expected OCI image index or Docker manifest list, found mediaType: '${media_type}'"
96+
exit 1
97+
fi
98+
echo "✅️ Verified multi-arch index (mediaType: ${media_type})."
3899

39-
if skopeo inspect --tls-verify=true "docker://${COMPLETE_PULLSPEC}" &>/dev/null; then
100+
for arch in ${optional_arches}; do
101+
if skopeo inspect --tls-verify=true --retry-times 3 --override-arch "${arch}" "docker://${COMPLETE_PULLSPEC}" &>/dev/null; then
102+
echo "✅️ Image '${COMPLETE_PULLSPEC}' can be inspected for arch ${arch}."
103+
else
104+
echo "🔴 Failed skopeo inspect for arch ${arch} on '${COMPLETE_PULLSPEC}'."
105+
skopeo inspect --tls-verify=true --retry-times 3 --override-arch "${arch}" "docker://${COMPLETE_PULLSPEC}"
106+
exit 1
107+
fi
108+
done
109+
elif skopeo inspect --tls-verify=true "docker://${COMPLETE_PULLSPEC}" &>/dev/null; then
40110
echo "✅️ Image '${COMPLETE_PULLSPEC}' can be pulled using skopeo."
41111
else
42112
echo "🔴 Failed to pull or inspect image '${COMPLETE_PULLSPEC}'."

0 commit comments

Comments
 (0)