Skip to content

Commit 805fc94

Browse files
fix(KONFLUX-10809): fail incrementer on empty tag list
Add extract_tags() that validates the list-tags response and checks repo existence via skopeo inspect when Tags is empty. If the repo exists but returned no tags, the task now fails instead of producing a potentially destructive increment value. Assisted-by: Claude Code Signed-off-by: Filip Nikolovski <fnikolov@redhat.com>
1 parent f557ad5 commit 805fc94

3 files changed

Lines changed: 175 additions & 3 deletions

File tree

tasks/managed/apply-mapping/apply-mapping.yaml

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,13 +198,42 @@ spec:
198198
exit 0
199199
fi
200200
201+
# Fetch and validate tags from a repository. Fails if the repo exists but
202+
# returns an empty tag list (likely a transient registry error) to prevent
203+
# silently falling back to increment 1 and overwriting existing tags.
204+
extract_tags() {
205+
local repo="$1"
206+
local raw_json
207+
raw_json=$(skopeo list-tags --retry-times 3 docker://"${repo}")
208+
209+
if ! jq -e '.Tags' <<< "${raw_json}" > /dev/null 2>&1; then
210+
echo "Error: skopeo list-tags returned invalid response (missing Tags key) for ${repo}" >&2
211+
exit 1
212+
fi
213+
214+
local tag_count
215+
tag_count=$(jq '.Tags | length' <<< "${raw_json}")
216+
217+
if [[ "${tag_count}" -eq 0 ]]; then
218+
if skopeo inspect --retry-times 3 --no-tags --raw docker://"${repo}" > /dev/null 2>&1; then
219+
echo "Error: Repository ${repo} exists but skopeo list-tags returned an empty tag list." >&2
220+
echo "This may indicate a transient registry error. Failing to prevent overwriting" >&2
221+
echo "existing tags by falling back to increment 1." >&2
222+
exit 1
223+
fi
224+
echo ""
225+
return
226+
fi
227+
228+
jq -r '.Tags[]' <<< "${raw_json}"
229+
}
230+
201231
# Function to handle incrementer logic
202232
increment_tag() {
203233
local tag_template="$1"
204234
local repo="$2"
205235
206-
# Use `skopeo list-tags` to fetch all tags from the repository
207-
existing_tags=$(skopeo list-tags --retry-times 3 docker://"${repo}" | jq -r '.Tags[]')
236+
existing_tags=$(extract_tags "${repo}")
208237
209238
# Remove `{{ incrementer }}` placeholder to get the version prefix for regex pattern
210239
# shellcheck disable=SC2001
@@ -284,7 +313,7 @@ spec:
284313
local repo
285314
repo=$(jq -r --argjson r "$r" '.[$r]' <<< "$all_repos_json")
286315
local existing_tags
287-
existing_tags=$(skopeo list-tags --retry-times 3 docker://"${repo}" | jq -r '.Tags[]')
316+
existing_tags=$(extract_tags "${repo}")
288317
local repo_max
289318
repo_max=$(echo "${existing_tags}" | { grep -E "${tag_pattern}" || true; } \
290319
| sed -E "s/^${escaped_prefix}//" | sort -nr | head -n1)

tasks/managed/apply-mapping/tests/mocks.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ function skopeo() {
8080
return
8181
fi
8282

83+
# Existing repo that transiently returns empty tags (simulates the bug scenario)
84+
if [[ "$*" =~ list-tags\ --retry-times\ 3\ docker://repo-empty-tags-existing ]]; then
85+
echo '{"Tags": []}'
86+
return
87+
fi
88+
8389
# Raw manifest inspections (for annotations and config.mediaType) - these use the digest from get-image-architectures
8490
if [[ "$*" == "inspect --retry-times 3 --no-tags --raw docker://quay.io/myorg/helm-chart"* ]]
8591
then
@@ -94,6 +100,12 @@ function skopeo() {
94100
then
95101
echo '{"config": {"mediaType": "application/vnd.oci.image.config.v1+json"}, "annotations": {"org.opencontainers.image.created": "2024-07-29T02:17:29Z"}}'
96102
return
103+
elif [[ "$*" == "inspect --retry-times 3 --no-tags --raw docker://repoa"* ]] || \
104+
[[ "$*" == "inspect --retry-times 3 --no-tags --raw docker://repo2"* ]]
105+
then
106+
# Genuinely new repos (no manifests yet) - inspect fails
107+
echo "Error: repository not found" >&2
108+
return 1
97109
elif [[ "$*" == "inspect --retry-times 3 --no-tags --raw docker://"* ]]
98110
then
99111
# Default: standard OCI container image config
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
---
2+
apiVersion: tekton.dev/v1
3+
kind: Pipeline
4+
metadata:
5+
name: test-apply-mapping-fail-empty-tags-existing-repo
6+
annotations:
7+
test/assert-task-failure: "run-task"
8+
spec:
9+
description: |
10+
Run the apply-mapping task with a component that uses {{ incrementer }} against a repository
11+
that exists but returns an empty tag list. The task should fail instead of silently falling
12+
back to increment 1, which could overwrite existing tags.
13+
params:
14+
- name: ociStorage
15+
description: The OCI repository where the Trusted Artifacts are stored.
16+
type: string
17+
- name: ociArtifactExpiresAfter
18+
description: Expiration date for the trusted artifacts created in the
19+
OCI repository. An empty string means the artifacts do not expire.
20+
type: string
21+
default: "1d"
22+
- name: orasOptions
23+
description: oras options to pass to Trusted Artifacts calls
24+
type: string
25+
default: "--insecure"
26+
- name: trustedArtifactsDebug
27+
description: Flag to enable debug logging in trusted artifacts. Set to a non-empty string to enable.
28+
type: string
29+
default: ""
30+
- name: dataDir
31+
description: The location where data will be stored
32+
type: string
33+
tasks:
34+
- name: setup
35+
taskSpec:
36+
results:
37+
- name: sourceDataArtifact
38+
type: string
39+
volumes:
40+
- name: workdir
41+
emptyDir: {}
42+
stepTemplate:
43+
volumeMounts:
44+
- mountPath: /var/workdir
45+
name: workdir
46+
env:
47+
- name: IMAGE_EXPIRES_AFTER
48+
value: $(params.ociArtifactExpiresAfter)
49+
- name: "ORAS_OPTIONS"
50+
value: "$(params.orasOptions)"
51+
- name: "DEBUG"
52+
value: "$(params.trustedArtifactsDebug)"
53+
steps:
54+
- name: setup-values
55+
image: quay.io/konflux-ci/release-service-utils@sha256:5546fa78d3c88d7b6a2e8cff8902f7757f00541d0bbaf113b9f293133894afa3
56+
script: |
57+
#!/usr/bin/env sh
58+
set -eux
59+
60+
mkdir -p "$(params.dataDir)/$(context.pipelineRun.uid)"
61+
cat > "$(params.dataDir)/$(context.pipelineRun.uid)/test_data.json" << EOF
62+
{
63+
"mapping": {
64+
"components": [
65+
{
66+
"name": "comp1",
67+
"repositories": [
68+
{
69+
"url": "repo-empty-tags-existing",
70+
"tags": [
71+
"v1.0.0-{{ incrementer }}"
72+
]
73+
}
74+
]
75+
}
76+
]
77+
}
78+
}
79+
EOF
80+
81+
cat > "$(params.dataDir)/$(context.pipelineRun.uid)/test_snapshot_spec.json" << EOF
82+
{
83+
"application": "myapp",
84+
"components": [
85+
{
86+
"name": "comp1",
87+
"containerImage": "registry.io/image1@sha256:123456",
88+
"source": {
89+
"git": {
90+
"revision": "testrevision",
91+
"url": "myurl"
92+
}
93+
}
94+
}
95+
]
96+
}
97+
EOF
98+
- name: create-trusted-artifact
99+
ref:
100+
name: create-trusted-artifact
101+
params:
102+
- name: ociStorage
103+
value: $(params.ociStorage)
104+
- name: workDir
105+
value: $(params.dataDir)
106+
- name: sourceDataArtifact
107+
value: $(results.sourceDataArtifact.path)
108+
- name: run-task
109+
taskRef:
110+
name: apply-mapping
111+
params:
112+
- name: snapshotPath
113+
value: $(context.pipelineRun.uid)/test_snapshot_spec.json
114+
- name: dataPath
115+
value: $(context.pipelineRun.uid)/test_data.json
116+
- name: ociStorage
117+
value: $(params.ociStorage)
118+
- name: orasOptions
119+
value: $(params.orasOptions)
120+
- name: sourceDataArtifact
121+
value: "$(tasks.setup.results.sourceDataArtifact)=$(params.dataDir)"
122+
- name: dataDir
123+
value: $(params.dataDir)
124+
- name: trustedArtifactsDebug
125+
value: $(params.trustedArtifactsDebug)
126+
- name: taskGitUrl
127+
value: "http://localhost"
128+
- name: taskGitRevision
129+
value: "main"
130+
runAfter:
131+
- setup

0 commit comments

Comments
 (0)