Skip to content

Commit 0fc6ae5

Browse files
committed
fix(KONFLUX-13012): clean up orphaned IRs before creating new ones
When the managed task is retried (e.g. due to timeout), the previous InternalRequest and its PipelineRun on the internal cluster continue running. The retry creates a new InternalRequest, leading to concurrent operations that cause duplicate Pulp pushes and CGW "Record already present" errors. Before creating a new InternalRequest, the script now deletes any existing InternalRequests with the same PipelineRun UID label. Combined with the finalizer change in internal-services(PR 709), this ensures the associated PipelineRun is cancelled before the new one starts. Assisted-by: Cursor AI Signed-off-by: Scott Wickersham <swickers@redhat.com>
1 parent edea34a commit 0fc6ae5

2 files changed

Lines changed: 161 additions & 0 deletions

File tree

utils/internal-request

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,37 @@ if [[ -n "${SERVICEACCOUNT}" ]]; then
264264
'.spec.serviceAccount = $serviceAccount' <<< $PAYLOAD)
265265
fi
266266

267+
# Clean up any existing InternalRequests from prior attempts within the same
268+
# PipelineRun. This handles cases where a managed task retry or timeout left
269+
# an orphaned InternalRequest whose PipelineRun may still be running on the
270+
# internal cluster. The internal-services controller will cancel the associated
271+
# PipelineRun when it detects the InternalRequest deletion.
272+
PIPELINERUN_UID_LABEL="internal-services.appstudio.openshift.io/pipelinerun-uid"
273+
PIPELINERUN_UID_VALUE=""
274+
for label in "${LABELS[@]}"; do
275+
KEY=$(echo "$label" | cut -d'=' -f1)
276+
VALUE=$(echo "$label" | cut -d'=' -f2-)
277+
if [ "$KEY" = "$PIPELINERUN_UID_LABEL" ]; then
278+
PIPELINERUN_UID_VALUE="$VALUE"
279+
break
280+
fi
281+
done
282+
283+
if [ -n "$PIPELINERUN_UID_VALUE" ]; then
284+
EXISTING_IRS=$(kubectl get internalrequest -l "${PIPELINERUN_UID_LABEL}=${PIPELINERUN_UID_VALUE}" \
285+
--no-headers -o custom-columns=":metadata.name" 2>/dev/null || true)
286+
287+
if [ -n "$EXISTING_IRS" ]; then
288+
echo "Found existing InternalRequests from prior attempts. Cleaning up..."
289+
for IR_NAME in $EXISTING_IRS; do
290+
echo "Deleting InternalRequest ${IR_NAME}..."
291+
kubectl delete internalrequest "$IR_NAME" 2>/dev/null || true
292+
done
293+
echo "Cleanup complete. Waiting for associated PipelineRuns to be cancelled..."
294+
sleep 5
295+
fi
296+
fi
297+
267298
# Create InternalRequest using kubectl
268299
RESOURCE=$(echo "$PAYLOAD" | kubectl create -f - -o json)
269300
INTERNAL_REQUEST_NAME=$(echo "$RESOURCE" | jq -r '.metadata.name')
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"""Tests for the internal-request utility script."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
import stat
7+
import subprocess
8+
from pathlib import Path
9+
10+
11+
SCRIPT_PATH = Path(__file__).resolve().parents[1] / "internal-request"
12+
13+
14+
def _write_executable(path: Path, content: str) -> None:
15+
path.write_text(content, encoding="utf-8")
16+
path.chmod(path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
17+
18+
19+
def _run_internal_request(
20+
tmp_path: Path, labels: list[str] | None = None, existing_irs: str = ""
21+
):
22+
bin_dir = tmp_path / "bin"
23+
bin_dir.mkdir()
24+
kubectl_log = tmp_path / "kubectl.log"
25+
sleep_log = tmp_path / "sleep.log"
26+
27+
_write_executable(
28+
bin_dir / "kubectl",
29+
"""#!/usr/bin/env bash
30+
set -euo pipefail
31+
echo "$*" >> "$KUBECTL_LOG"
32+
if [[ "${1:-}" == "get" && "${2:-}" == "internalrequest" ]]; then
33+
printf "%s\\n" "${MOCK_EXISTING_IRS:-}"
34+
exit 0
35+
fi
36+
if [[ "${1:-}" == "delete" && "${2:-}" == "internalrequest" ]]; then
37+
exit 0
38+
fi
39+
if [[ "${1:-}" == "create" ]]; then
40+
cat >/dev/null
41+
echo '{"metadata":{"name":"new-ir"}}'
42+
exit 0
43+
fi
44+
exit 1
45+
""",
46+
)
47+
48+
_write_executable(
49+
bin_dir / "sleep",
50+
"""#!/usr/bin/env bash
51+
set -euo pipefail
52+
echo "$*" >> "$SLEEP_LOG"
53+
""",
54+
)
55+
56+
cmd = [
57+
"bash",
58+
str(SCRIPT_PATH),
59+
"--pipeline",
60+
"test-pipeline",
61+
"-p",
62+
"taskGitUrl=https://github.com/konflux-ci/release-service-catalog",
63+
"-p",
64+
"taskGitRevision=main",
65+
"-s",
66+
"false",
67+
]
68+
for label in labels or []:
69+
cmd.extend(["-l", label])
70+
71+
env = os.environ.copy()
72+
env["PATH"] = f"{bin_dir}:{env['PATH']}"
73+
env["KUBECTL_LOG"] = str(kubectl_log)
74+
env["SLEEP_LOG"] = str(sleep_log)
75+
env["MOCK_EXISTING_IRS"] = existing_irs
76+
77+
result = subprocess.run(cmd, capture_output=True, text=True, check=False, env=env)
78+
79+
kubectl_calls = kubectl_log.read_text(encoding="utf-8").splitlines()
80+
sleep_calls = (
81+
sleep_log.read_text(encoding="utf-8").splitlines() if sleep_log.exists() else []
82+
)
83+
return result, kubectl_calls, sleep_calls
84+
85+
86+
def test_internal_request_cleans_up_existing_requests(tmp_path):
87+
result, kubectl_calls, sleep_calls = _run_internal_request(
88+
tmp_path=tmp_path,
89+
labels=["internal-services.appstudio.openshift.io/pipelinerun-uid=uid-123"],
90+
existing_irs="old-ir-1\nold-ir-2",
91+
)
92+
93+
assert result.returncode == 0, result.stderr
94+
selector = (
95+
"get internalrequest -l "
96+
"internal-services.appstudio.openshift.io/pipelinerun-uid=uid-123"
97+
)
98+
assert any(call.startswith(selector) for call in kubectl_calls)
99+
assert "delete internalrequest old-ir-1" in kubectl_calls
100+
assert "delete internalrequest old-ir-2" in kubectl_calls
101+
assert "5" in sleep_calls
102+
assert any(call.startswith("create -f - -o json") for call in kubectl_calls)
103+
104+
105+
def test_internal_request_skips_cleanup_when_no_existing_requests(tmp_path):
106+
result, kubectl_calls, sleep_calls = _run_internal_request(
107+
tmp_path=tmp_path,
108+
labels=["internal-services.appstudio.openshift.io/pipelinerun-uid=uid-123"],
109+
existing_irs="",
110+
)
111+
112+
assert result.returncode == 0, result.stderr
113+
assert any(call.startswith("get internalrequest -l") for call in kubectl_calls)
114+
assert not any(call.startswith("delete internalrequest") for call in kubectl_calls)
115+
assert sleep_calls == []
116+
assert any(call.startswith("create -f - -o json") for call in kubectl_calls)
117+
118+
119+
def test_internal_request_skips_cleanup_without_pipelinerun_uid_label(tmp_path):
120+
result, kubectl_calls, sleep_calls = _run_internal_request(
121+
tmp_path=tmp_path,
122+
labels=["some-other-label=foo"],
123+
existing_irs="old-ir-1",
124+
)
125+
126+
assert result.returncode == 0, result.stderr
127+
assert not any(call.startswith("get internalrequest -l") for call in kubectl_calls)
128+
assert not any(call.startswith("delete internalrequest") for call in kubectl_calls)
129+
assert sleep_calls == []
130+
assert any(call.startswith("create -f - -o json") for call in kubectl_calls)

0 commit comments

Comments
 (0)