Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions tasks/managed/apply-mapping/apply-mapping.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,42 @@ spec:
exit 0
fi

# Fetch and validate tags from a repository. Fails if the repo exists but
# returns an empty tag list (likely a transient registry error) to prevent
# silently falling back to increment 1 and overwriting existing tags.
extract_tags() {
local repo="$1"
local raw_json
raw_json=$(skopeo list-tags --retry-times 3 docker://"${repo}")

if ! jq -e '.Tags' <<< "${raw_json}" > /dev/null 2>&1; then
echo "Error: skopeo list-tags returned invalid response (missing Tags key) for ${repo}" >&2
exit 1
fi

local tag_count
tag_count=$(jq '.Tags | length' <<< "${raw_json}")

if [[ "${tag_count}" -eq 0 ]]; then
if skopeo inspect --retry-times 3 --no-tags --raw docker://"${repo}" > /dev/null 2>&1; then
echo "Error: Repository ${repo} exists but skopeo list-tags returned an empty tag list." >&2
echo "This may indicate a transient registry error. Failing to prevent overwriting" >&2
echo "existing tags by falling back to increment 1." >&2
exit 1
fi
echo ""
return
fi

jq -r '.Tags[]' <<< "${raw_json}"
}

# Function to handle incrementer logic
increment_tag() {
local tag_template="$1"
local repo="$2"

# Use `skopeo list-tags` to fetch all tags from the repository
existing_tags=$(skopeo list-tags --retry-times 3 docker://"${repo}" | jq -r '.Tags[]')
existing_tags=$(extract_tags "${repo}") || exit 1

# Remove `{{ incrementer }}` placeholder to get the version prefix for regex pattern
# shellcheck disable=SC2001
Expand Down Expand Up @@ -284,7 +313,7 @@ spec:
local repo
repo=$(jq -r --argjson r "$r" '.[$r]' <<< "$all_repos_json")
local existing_tags
existing_tags=$(skopeo list-tags --retry-times 3 docker://"${repo}" | jq -r '.Tags[]')
existing_tags=$(extract_tags "${repo}") || exit 1
local repo_max
repo_max=$(echo "${existing_tags}" | { grep -E "${tag_pattern}" || true; } \
| sed -E "s/^${escaped_prefix}//" | sort -nr | head -n1)
Expand Down
17 changes: 17 additions & 0 deletions tasks/managed/apply-mapping/tests/mocks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ function skopeo() {
return
fi

# Existing repo that transiently returns empty tags (simulates the bug scenario)
if [[ "$*" =~ list-tags\ --retry-times\ 3\ docker://repo-empty-tags-existing ]]; then
echo '{"Tags": []}'
return
fi

# Raw manifest inspections (for annotations and config.mediaType) - these use the digest from get-image-architectures
if [[ "$*" == "inspect --retry-times 3 --no-tags --raw docker://quay.io/myorg/helm-chart"* ]]
then
Expand All @@ -94,6 +100,17 @@ function skopeo() {
then
echo '{"config": {"mediaType": "application/vnd.oci.image.config.v1+json"}, "annotations": {"org.opencontainers.image.created": "2024-07-29T02:17:29Z"}}'
return
elif [[ "$*" == "inspect --retry-times 3 --no-tags --raw docker://repo-empty-tags-existing"* ]]
then
# Existing repo that transiently returns empty tags - inspect succeeds
echo '{"config": {"mediaType": "application/vnd.oci.image.config.v1+json"}, "annotations": {}}'
return
elif [[ "$*" == "inspect --retry-times 3 --no-tags --raw docker://repoa"* ]] || \
[[ "$*" == "inspect --retry-times 3 --no-tags --raw docker://repo2"* ]]
then
# Genuinely new repos (no manifests yet) - inspect fails
echo "Error: repository not found" >&2
return 1
Comment on lines +108 to +113

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice fix!
One small suggestion: consider adding an explicit mock for repo-empty-tags-existing for the skopeo inspect call. Currently it works via the default fallthrough, but making it explicit improves test clarity:

Suggested change
elif [[ "$*" == "inspect --retry-times 3 --no-tags --raw docker://repoa"* ]] || \
[[ "$*" == "inspect --retry-times 3 --no-tags --raw docker://repo2"* ]]
then
# Genuinely new repos (no manifests yet) - inspect fails
echo "Error: repository not found" >&2
return 1
elif [[ "$*" == "inspect --retry-times 3 --no-tags --raw docker://repo-empty-tags-existing"* ]]
then
# Existing repo that transiently returns empty tags - inspect succeeds
echo '{"config": {"mediaType": "application/vnd.oci.image.config.v1+json"}, "annotations": {}}'
return
elif [[ "$*" == "inspect --retry-times 3 --no-tags --raw docker://repoa"* ]] || \
[[ "$*" == "inspect --retry-times 3 --no-tags --raw docker://repo2"* ]]
then
# Genuinely new repos (no manifests yet) - inspect fails
echo "Error: repository not found" >&2
return 1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, updated!

elif [[ "$*" == "inspect --retry-times 3 --no-tags --raw docker://"* ]]
then
# Default: standard OCI container image config
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
---
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: test-apply-mapping-fail-empty-tags-existing-repo
annotations:
test/assert-task-failure: "run-task"
spec:
description: |
Run the apply-mapping task with a component that uses {{ incrementer }} against a repository
that exists but returns an empty tag list. The task should fail instead of silently falling
back to increment 1, which could overwrite existing tags.
params:
- name: ociStorage
description: The OCI repository where the Trusted Artifacts are stored.
type: string
- name: ociArtifactExpiresAfter
description: Expiration date for the trusted artifacts created in the
OCI repository. An empty string means the artifacts do not expire.
type: string
default: "1d"
- name: orasOptions
description: oras options to pass to Trusted Artifacts calls
type: string
default: "--insecure"
- name: trustedArtifactsDebug
description: Flag to enable debug logging in trusted artifacts. Set to a non-empty string to enable.
type: string
default: ""
- name: dataDir
description: The location where data will be stored
type: string
tasks:
- name: setup
taskSpec:
results:
- name: sourceDataArtifact
type: string
volumes:
- name: workdir
emptyDir: {}
stepTemplate:
volumeMounts:
- mountPath: /var/workdir
name: workdir
env:
- name: IMAGE_EXPIRES_AFTER
value: $(params.ociArtifactExpiresAfter)
- name: "ORAS_OPTIONS"
value: "$(params.orasOptions)"
- name: "DEBUG"
value: "$(params.trustedArtifactsDebug)"
steps:
- name: setup-values
image: quay.io/konflux-ci/release-service-utils@sha256:5546fa78d3c88d7b6a2e8cff8902f7757f00541d0bbaf113b9f293133894afa3
script: |
#!/usr/bin/env sh
set -eux

mkdir -p "$(params.dataDir)/$(context.pipelineRun.uid)"
cat > "$(params.dataDir)/$(context.pipelineRun.uid)/test_data.json" << EOF
{
"mapping": {
"components": [
{
"name": "comp1",
"repositories": [
{
"url": "repo-empty-tags-existing",
"tags": [
"v1.0.0-{{ incrementer }}"
]
}
]
}
]
}
}
EOF

cat > "$(params.dataDir)/$(context.pipelineRun.uid)/test_snapshot_spec.json" << EOF
{
"application": "myapp",
"components": [
{
"name": "comp1",
"containerImage": "registry.io/image1@sha256:123456",
"source": {
"git": {
"revision": "testrevision",
"url": "myurl"
}
}
}
]
}
EOF
- name: create-trusted-artifact
ref:
name: create-trusted-artifact
params:
- name: ociStorage
value: $(params.ociStorage)
- name: workDir
value: $(params.dataDir)
- name: sourceDataArtifact
value: $(results.sourceDataArtifact.path)
- name: run-task
taskRef:
name: apply-mapping
params:
- name: snapshotPath
value: $(context.pipelineRun.uid)/test_snapshot_spec.json
- name: dataPath
value: $(context.pipelineRun.uid)/test_data.json
- name: ociStorage
value: $(params.ociStorage)
- name: orasOptions
value: $(params.orasOptions)
- name: sourceDataArtifact
value: "$(tasks.setup.results.sourceDataArtifact)=$(params.dataDir)"
- name: dataDir
value: $(params.dataDir)
- name: trustedArtifactsDebug
value: $(params.trustedArtifactsDebug)
- name: taskGitUrl
value: "http://localhost"
- name: taskGitRevision
value: "main"
runAfter:
- setup
Loading