Skip to content

Commit 0bfa911

Browse files
johnbierenAuthor
authored andcommitted
refactor(RELEASE-1985): convert create-advisory internal task to python
This commit replaces the inline bash script for the create-advisory internal task with a standalone python script contained in the utils image. The tekton unit tests are updated accordingly (mocks converted to a way that works with the python script and test scenarios removed that are already covered via pytest in the utils repo). Signed-off-by: Johnny Bieren <jbieren@redhat.com>
1 parent 6929647 commit 0bfa911

20 files changed

Lines changed: 148 additions & 1230 deletions

.github/scripts/mock_http_json.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,26 +41,27 @@ def log_message(self, *_args: object) -> None:
4141
# Keep Tekton step logs readable; every GET would otherwise print a line.
4242
return
4343

44-
def do_GET(self) -> None:
44+
def _route_body(self) -> bytes | None:
4545
parsed = urlparse(self.path)
4646
path = parsed.path.rstrip("/") or "/"
47-
body = None
4847
# Order in mocks.yaml matters: first matching rule wins (not most specific).
4948
for rule in self.routes:
5049
suf = rule.get("path_suffix")
5150
if suf is not None:
5251
# Match both "/auth/token" and "/auth/token/" style paths.
5352
if path.endswith(suf) or path.endswith(suf.rstrip("/")):
5453
# mocks.yaml body values are strings, not pre-serialized bytes.
55-
body = rule["body"].encode("utf-8")
56-
break
54+
return rule["body"].encode("utf-8")
5755
# path_suffix and path_contains are mutually exclusive per rule.
5856
continue
5957
sub = rule.get("path_contains")
6058
if sub is not None and sub in parsed.path:
6159
# Query string is ignored; only the path is checked.
62-
body = rule["body"].encode("utf-8")
63-
break
60+
return rule["body"].encode("utf-8")
61+
return None
62+
63+
def _send_routed_json(self) -> None:
64+
body = self._route_body()
6465
if body is None:
6566
# Unmatched paths look like "service down" to callers, not empty JSON.
6667
self.send_response(404)
@@ -72,6 +73,8 @@ def do_GET(self) -> None:
7273
self.end_headers()
7374
self.wfile.write(body)
7475

76+
do_GET = do_POST = _send_routed_json
77+
7578

7679
class _ReuseHTTPServer(HTTPServer):
7780
# Lets the test step restart the mock without "Address already in use".

tasks/internal/create-advisory-task/create-advisory-task.yaml

Lines changed: 28 additions & 261 deletions
Original file line numberDiff line numberDiff line change
@@ -86,274 +86,41 @@ spec:
8686
runAsUser: 1001
8787
steps:
8888
- name: create-advisory
89-
image: quay.io/konflux-ci/release-service-utils@sha256:9460d206ab78a096679cf0d96bf812b3f9a5227dd2f7061e06e8e58c49cdad16
89+
image: quay.io/jbieren/release-service-utils:createadv_5
9090
computeResources:
9191
limits:
92-
memory: 256Mi
92+
memory: 1Gi
9393
requests:
94-
memory: 256Mi
94+
memory: 1Gi
9595
cpu: '1' # 1 is the max allowed by at least the staging cluster
9696
volumeMounts:
9797
- name: advisory-secret
9898
mountPath: /mnt/advisory_secret
9999
- name: errata-secret
100100
mountPath: /mnt/errata_secret
101101
env:
102-
- name: "ADVISORY_JSON"
103-
value: "$(params.advisory_json)"
104-
script: |
105-
#!/usr/bin/env bash
106-
set -eo pipefail
107-
108-
GITLAB_HOST="$(cat /mnt/advisory_secret/gitlab_host)"
109-
110-
# This is a GitLab Project access token. Go to the settings/access_tokens page
111-
# of your repository to create one. It should have the Developer role with read
112-
# and write repository rights.
113-
ACCESS_TOKEN="$(cat /mnt/advisory_secret/gitlab_access_token)"
114-
115-
GIT_AUTHOR_NAME="$(cat /mnt/advisory_secret/git_author_name)"
116-
GIT_AUTHOR_EMAIL="$(cat /mnt/advisory_secret/git_author_email)"
117-
GIT_REPO="$(cat /mnt/advisory_secret/git_repo)"
118-
ERRATA_API="$(cat /mnt/errata_secret/errata_api)"
119-
SERVICE_ACCOUNT_NAME="$(cat /mnt/errata_secret/name)"
120-
SERVICE_ACCOUNT_KEYTAB="$(cat /mnt/errata_secret/base64_keytab)"
121-
122-
# export variables required by the called script "gitlab-functions" in release-service-utils
123-
export GITLAB_HOST ACCESS_TOKEN GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
124-
125-
STDERR_FILE=/tmp/stderr.txt
126-
echo -n "$(params.internalRequestPipelineRunName)" > "$(results.internalRequestPipelineRunName.path)"
127-
echo -n "$(context.taskRun.name)" > "$(results.internalRequestTaskRunName.path)"
128-
129-
exitfunc() {
130-
local err=$1
131-
local line=$2
132-
local command="$3"
133-
if [ "$err" -eq 0 ] ; then
134-
echo -n "Success" > "$(results.result.path)"
135-
else
136-
echo -n \
137-
"$0: ERROR '$command' failed at line $line - exited with status $err" > "$(results.result.path)"
138-
if [ -f "$STDERR_FILE" ] ; then
139-
tail -n 20 "$STDERR_FILE" >> "$(results.result.path)"
140-
fi
141-
fi
142-
echo -n "${ADVISORY_URL}" > "$(results.advisory_url.path)"
143-
echo -n "${ADVISORY_INTERNAL_URL}" > "$(results.advisory_internal_url.path)"
144-
exit 0 # exit the script cleanly as there is no point in proceeding past an error or exit call
145-
}
146-
# due to set -e, this catches all EXIT and ERR calls and the task should never fail with nonzero exit code
147-
trap 'exitfunc $? $LINENO "$BASH_COMMAND"' EXIT
148-
149-
REPO_BRANCH=main
150-
ADVISORY_URL=""
151-
ADVISORY_INTERNAL_URL=""
152-
ADVISORY_BASE_DIR="data/advisories/$(params.origin)"
153-
if [[ "${GIT_REPO}" == *"/rhtap-release/"* ]]; then
154-
ADVISORY_URL_PREFIX="https://access.stage.redhat.com/errata"
155-
else
156-
ADVISORY_URL_PREFIX="https://access.redhat.com/errata"
157-
fi
158-
159-
# Switch to /tmp to avoid filesystem permission issues
160-
cd /tmp
161-
162-
# loading git and gitlab functions
163-
# shellcheck source=/dev/null
164-
. /home/utils/gitlab-functions
165-
# shellcheck source=/dev/null
166-
. /home/utils/git-functions
167-
gitlab_init
168-
git_functions_init
169-
170-
# This also cds into the git repo
171-
git_clone_and_checkout --repository "$GIT_REPO" --revision "$REPO_BRANCH" \
172-
--sparse-dir "$ADVISORY_BASE_DIR" --sparse-dir schema
173-
174-
if [ "$(params.contentType)" = "image" ]; then
175-
echo "Content type is image."
176-
spec_content_type=".content.images"
177-
elif [ "$(params.contentType)" == "binary" ] || [ "$(params.contentType)" == "generic" ] \
178-
|| [ "$(params.contentType)" == "rpm" ]; then
179-
echo "Content type is generic or rpm artifact."
180-
spec_content_type=".content.artifacts"
181-
else
182-
echo "Unsupported contentType: $(params.contentType)"| tee -a "$STDERR_FILE"
183-
echo "Exiting." | tee -a "$STDERR_FILE"
184-
exit 1
185-
fi
186-
CONTENT_FILE=/tmp/content.json
187-
# Write the advisory JSON parameter to a file to avoid argument length limits
188-
printf '%s' "$ADVISORY_JSON" | base64 --decode | gunzip > /tmp/advisory_decoded.json
189-
jq -c "${spec_content_type} // []" /tmp/advisory_decoded.json > "$CONTENT_FILE"
190-
191-
# Use ISO 8601 format in UTC/Zulu time, e.g. 2024-03-06T17:27:38Z
192-
SHIP_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
193-
YEAR=${SHIP_DATE%%-*} # derive the year from the ship date
194-
# Define advisory directory
195-
echo "Checking advisories in directory: ${ADVISORY_BASE_DIR}"
196-
197-
# Check existing advisories across ALL years
198-
EXISTING_ADVISORIES=""
199-
if [ -d "${ADVISORY_BASE_DIR}" ]; then
200-
EXISTING_ADVISORIES=$(
201-
# year/advisory dir with modified time
202-
find "${ADVISORY_BASE_DIR}" -mindepth 2 -type d -printf "%T@ %p\n" |
203-
sort -nr | # sort by latest modified first
204-
cut -d' ' -f2- | # remove the timestamp, keep only path
205-
sed "s|^${ADVISORY_BASE_DIR}/||" # keeping year/advisory format
206-
)
207-
fi
208-
209-
if [[ -z "$EXISTING_ADVISORIES" ]]; then
210-
echo "No existing advisories found."
211-
fi
212-
213-
# Track the latest advisory that contains matching content
214-
# EXISTING_ADVISORIES is sorted by modification time (newest first)
215-
LATEST_ADVISORY_FILE=""
216-
217-
EXISTING_CONTENT=/tmp/existing_content.json
218-
for ADVISORY_SUBDIR in $EXISTING_ADVISORIES; do
219-
ADVISORY_FILE="${ADVISORY_BASE_DIR}/${ADVISORY_SUBDIR}/advisory.yaml"
220-
yq -o=json ".spec${spec_content_type} // []" "${ADVISORY_FILE}" > "$EXISTING_CONTENT"
221-
echo "Processing advisory: ${ADVISORY_FILE}"
222-
echo "Existing content in advisory: "
223-
cat "$EXISTING_CONTENT"
224-
225-
# Check if this advisory contains any matching content before filtering
226-
CONTENT_BEFORE_FILTER=$(cat "$CONTENT_FILE")
227-
228-
# Update CONTENT by removing entries that already exist in the advisory
229-
if [ "$(params.contentType)" == "generic" ] || [ "$(params.contentType)" == "binary" ]; then
230-
# Use purl as unique key, but strip checksum= for comparison
231-
# This allows re-releases (with new checksums from re-signing) to update existing advisories
232-
# The filename= param (if present) ensures we match the correct file
233-
jq --slurpfile existing "$EXISTING_CONTENT" '
234-
# Function to strip checksum param from purl for comparison
235-
def strip_checksum:
236-
gsub("&checksum=[^&]*"; "") | gsub("\\?checksum=[^&]*&"; "?") | gsub("\\?checksum=[^&]*$"; "");
237-
map(select(
238-
(.purl | strip_checksum) as $p |
239-
($existing[0] | map(select((.purl | strip_checksum) == $p)) | length == 0)
240-
))' "$CONTENT_FILE" > /tmp/content_filtered.json
241-
elif [ "$(params.contentType)" == "rpm" ] || [ "$(params.contentType)" == "disk-image" ]; then
242-
# Use exact purl matching for RPM and disk-image (checksums are stable, no re-signing)
243-
jq --slurpfile existing "$EXISTING_CONTENT" '
244-
map(select(
245-
.purl as $p |
246-
($existing[0] | map(select(.purl == $p)) | length == 0)
247-
))' "$CONTENT_FILE" > /tmp/content_filtered.json
248-
else
249-
jq --slurpfile existing "$EXISTING_CONTENT" '
250-
map(select(
251-
.containerImage as $ci |
252-
.tags as $tags |
253-
.repository as $repo |
254-
($existing[0] | map(select(
255-
.containerImage == $ci and .tags == $tags and .repository == $repo
256-
)) | length == 0)
257-
))' "$CONTENT_FILE" > /tmp/content_filtered.json
258-
fi
259-
260-
mv /tmp/content_filtered.json "$CONTENT_FILE"
261-
262-
echo "Remaining entries after filtering:"
263-
cat "$CONTENT_FILE"
264-
265-
CONTENT_BEFORE_COUNT=$(jq 'length' <<< "$CONTENT_BEFORE_FILTER")
266-
CONTENT_AFTER_COUNT=$(jq 'length' "$CONTENT_FILE")
267-
if [[ $CONTENT_BEFORE_COUNT -gt $CONTENT_AFTER_COUNT ]]; then
268-
if [[ -z "$LATEST_ADVISORY_FILE" ]]; then
269-
LATEST_ADVISORY_FILE="$ADVISORY_FILE"
270-
FILTERED_COUNT=$((CONTENT_BEFORE_COUNT - CONTENT_AFTER_COUNT))
271-
echo "Tracked latest advisory: $LATEST_ADVISORY_FILE (filtered $FILTERED_COUNT items)"
272-
fi
273-
fi
274-
275-
# If after filtering, no entries are left, then we can exit early
276-
if jq -e 'length == 0' "$CONTENT_FILE" >/dev/null; then
277-
echo "All content found in existing advisories. Skipping creation."
278-
echo "Returning advisory: $LATEST_ADVISORY_FILE"
279-
280-
ADVISORY_INTERNAL_URL="${GIT_REPO//\.git/}/-/raw/main/${LATEST_ADVISORY_FILE}"
281-
ADVISORY_TYPE=$(yq -r '.spec.type' "${LATEST_ADVISORY_FILE}")
282-
ADVISORY_NAME=$(yq -r '.metadata.name' "${LATEST_ADVISORY_FILE}")
283-
ADVISORY_URL="${ADVISORY_URL_PREFIX}/${ADVISORY_TYPE}-${ADVISORY_NAME}"
284-
echo -n "Success" > "$(results.result.path)"
285-
echo -n "${ADVISORY_URL}" > "$(results.advisory_url.path)"
286-
echo -n "$ADVISORY_INTERNAL_URL" > "$(results.advisory_internal_url.path)"
287-
exit 0
288-
fi
289-
done
290-
291-
NEW_ADVISORY_JSON=$(jq --slurpfile new_content "$CONTENT_FILE" \
292-
"${spec_content_type} = \$new_content[0]" /tmp/advisory_decoded.json)
293-
294-
signingKey=$(kubectl get configmap "$(params.config_map_name)" -o jsonpath="{.data.SIG_KEY_NAME}")
295-
# Write to temp file to avoid argument length limits
296-
echo "$NEW_ADVISORY_JSON" > /tmp/new_advisory.json
297-
jq -c --arg key "$signingKey" \
298-
"${spec_content_type}[] += {\"signingKey\": \$key}" /tmp/new_advisory.json > /tmp/advisory_with_key.json
299-
300-
LIVE_ID=$(jq -r '.live_id' /tmp/advisory_decoded.json)
301-
if [[ "$LIVE_ID" == null ]]; then
302-
# write keytab to file
303-
echo -n "${SERVICE_ACCOUNT_KEYTAB}" | base64 --decode > /tmp/keytab
304-
# workaround kinit: Invalid UID in persistent keyring name while getting default ccache
305-
KRB5CCNAME=$(mktemp)
306-
export KRB5CCNAME
307-
# see https://stackoverflow.com/a/12308187
308-
KRB5_CONFIG=$(mktemp)
309-
export KRB5_CONFIG
310-
export KRB5_TRACE=/dev/stderr
311-
sed '/\[libdefaults\]/a\ dns_canonicalize_hostname = false' /etc/krb5.conf > "${KRB5_CONFIG}"
312-
retry 5 kinit "${SERVICE_ACCOUNT_NAME}" -k -t /tmp/keytab
313-
REQUEST_URL="${ERRATA_API}/advisory/reserve_live_id"
314-
LIVE_ID=$(curl --retry 3 --negotiate -u : "${REQUEST_URL}" -XPOST | jq -r '.live_id')
315-
fi
316-
ADVISORY_NUM=$(printf "%04d" "$LIVE_ID")
317-
318-
# Check if the advisory number is already used
319-
GIT_RESULT_FILE=$(mktemp)
320-
git ls-tree -r --name-only origin/main > "$GIT_RESULT_FILE"
321-
GREP_RESULT=$(grep "data/advisories/.*/${YEAR}/${ADVISORY_NUM}/" "$GIT_RESULT_FILE" || true)
322-
if [[ -n "${GREP_RESULT}" ]]; then
323-
echo "An advisory with number ${ADVISORY_NUM} already exists:" | tee -a "$STDERR_FILE"
324-
echo "${GREP_RESULT}" | tee -a "$STDERR_FILE"
325-
echo "Exiting." | tee -a "$STDERR_FILE"
326-
exit 1
327-
fi
328-
329-
# group advisories by <origin workspace>/year
330-
ADVISORY_DIR="data/advisories/$(params.origin)/${YEAR}/${ADVISORY_NUM}"
331-
mkdir -p "${ADVISORY_DIR}"
332-
JSON_ADVISORY_FILEPATH="${ADVISORY_DIR}/advisory.json"
333-
YAML_ADVISORY_FILEPATH="${ADVISORY_DIR}/advisory.yaml"
334-
ADVISORY_NAME="${YEAR}:${ADVISORY_NUM}"
335-
336-
# Prepare variables for the advisory template
337-
# Write to file to avoid argument length limits
338-
jq -c '{"advisory":{"spec":.}}' /tmp/advisory_with_key.json > /tmp/template_data.json
339-
jq -c --arg advisory_name "$ADVISORY_NAME" --arg advisory_ship_date "$SHIP_DATE" \
340-
'$ARGS.named + .' /tmp/template_data.json > /tmp/template_data_final.json
341-
342-
# Create advisory file using the apply_template.py script
343-
/home/utils/apply_template.py -o "$JSON_ADVISORY_FILEPATH" --data-file /tmp/template_data_final.json \
344-
--template /home/templates/advisory.yaml.jinja -v 2> "$STDERR_FILE"
345-
346-
# Convert to yaml for readability
347-
yq eval -o yaml "$JSON_ADVISORY_FILEPATH" | tee "$YAML_ADVISORY_FILEPATH"
348-
349-
# Ensure the created advisory file passes the advisory schema
350-
check-jsonschema --schemafile schema/advisory.json "$YAML_ADVISORY_FILEPATH" 2>&1 | tee "$STDERR_FILE"
351-
352-
git add "${YAML_ADVISORY_FILEPATH}"
353-
git commit -m "[Konflux Release] new advisory for $(params.componentGroup)"
354-
echo "Pushing to ${REPO_BRANCH}..."
355-
git_push_with_retries --branch $REPO_BRANCH --retries 5 --url origin 2> "$STDERR_FILE"
356-
# Construct the advisory url on customer portal to report back to the user as a result
357-
ADVISORY_TYPE=$(jq -r '.type' /tmp/advisory_decoded.json)
358-
ADVISORY_URL="${ADVISORY_URL_PREFIX}/${ADVISORY_TYPE}-${ADVISORY_NAME}"
359-
ADVISORY_INTERNAL_URL="${GIT_REPO//\.git/}/-/raw/${REPO_BRANCH}/${YAML_ADVISORY_FILEPATH}"
102+
- name: ADVISORY_JSON
103+
value: $(params.advisory_json)
104+
- name: PARAM_COMPONENT_GROUP
105+
value: $(params.componentGroup)
106+
- name: PARAM_ORIGIN
107+
value: $(params.origin)
108+
- name: PARAM_CONFIG_MAP_NAME
109+
value: $(params.config_map_name)
110+
- name: PARAM_CONTENT_TYPE
111+
value: $(params.contentType)
112+
- name: PARAM_INTERNAL_REQUEST_PIPELINE_RUN_NAME
113+
value: $(params.internalRequestPipelineRunName)
114+
- name: PARAM_TASK_RUN_NAME
115+
value: $(context.taskRun.name)
116+
- name: RESULT_RESULT
117+
value: $(results.result.path)
118+
- name: RESULT_ADVISORY_URL
119+
value: $(results.advisory_url.path)
120+
- name: RESULT_ADVISORY_INTERNAL_URL
121+
value: $(results.advisory_internal_url.path)
122+
- name: RESULT_INTERNAL_REQUEST_PIPELINE_RUN_NAME
123+
value: $(results.internalRequestPipelineRunName.path)
124+
- name: RESULT_INTERNAL_REQUEST_TASK_RUN_NAME
125+
value: $(results.internalRequestTaskRunName.path)
126+
command: ["/home/scripts/python/tasks/internal/create_advisory.py"]

0 commit comments

Comments
 (0)