Skip to content
Draft
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
7 changes: 6 additions & 1 deletion .github/scripts/mock_http_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,16 @@ def main() -> None:
bind = os.environ.get("TEKTON_HTTP_BIND", "127.0.0.1")
routes = _routes_from_env()
_Handler.routes = routes
server = _ReuseHTTPServer((bind, port), _Handler)

cert_path: str | None = None
key_path: str | None = None
if os.environ.get("TEKTON_MOCK_TLS") == "1":
hostname = os.environ.get("TEKTON_MOCK_HOSTNAME")
cert_path, key_path, _ca = _generate_self_signed_cert(bind, hostname)

server = _ReuseHTTPServer((bind, port), _Handler)

if cert_path is not None and key_path is not None:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(cert_path, key_path)
server.socket = ctx.wrap_socket(server.socket, server_side=True)
Expand Down
50 changes: 40 additions & 10 deletions .github/scripts/render_python_task_mocks_from_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,14 +173,6 @@ def _render_http_json(svc: dict[str, Any]) -> str:
"",
]

if use_tls:
lines += [
'export SSL_CERT_FILE="/tmp/mock-ca.crt"',
'export REQUESTS_CA_BUNDLE="/tmp/mock-ca.crt"',
'export CURL_CA_BUNDLE="/tmp/mock-ca.crt"',
"",
]

mock_server_for_env_var = svc.get("mock_server_for_env_var")
if mock_server_for_env_var is not None:
if not isinstance(mock_server_for_env_var, str) or not _ENV_SAFE.match(
Expand Down Expand Up @@ -241,6 +233,36 @@ def _render_http_json(svc: dict[str, Any]) -> str:
return "\n".join(lines)


def _render_tls_task_entrypoint_exec() -> str:
"""Run TASK_ENTRYPOINT under Python after configuring TLS trust for mock HTTPS."""
return r"""exec python3 - "${TASK_ENTRYPOINT[@]}" <<'PY'
import os
import runpy
import sys
import tempfile

ca = os.environ.get("TEKTON_MOCK_CA_CERT_PATH", "")
if ca and os.path.isfile(ca):
bundle = ca
system_ca = "/etc/pki/tls/certs/ca-bundle.crt"
if os.path.isfile(system_ca):
fd, bundle = tempfile.mkstemp(suffix=".crt")
os.close(fd)
with open(bundle, "wb") as out:
with open(system_ca, "rb") as handle:
out.write(handle.read())
with open(ca, "rb") as handle:
out.write(handle.read())
os.environ["SSL_CERT_FILE"] = bundle
os.environ["REQUESTS_CA_BUNDLE"] = bundle
os.environ["CURL_CA_BUNDLE"] = bundle

script = sys.argv[1]
sys.argv = [script, *sys.argv[2:]]
raise SystemExit(runpy.run_path(script, run_name="__main__") or 0)
PY"""


def _render_mock_binaries_from_dir(tests_dir: Path) -> str:
"""Turn ``tests/mocks/<name>`` files into a temp dir prepended to ``PATH``."""
mock_dir = tests_dir / "mocks"
Expand Down Expand Up @@ -322,11 +344,19 @@ def render(tests_dir: Path) -> str:
_die("at most one http_json service (v1)")

parts: list[str] = []
use_tls_entrypoint = False
if http_svcs:
parts.append(_render_http_json(http_svcs[0]))
svc = http_svcs[0]
parts.append(_render_http_json(svc))
use_tls_entrypoint = bool(svc.get("tls", False)) and svc.get(
"rewrite_ca_cert_env"
) is None
parts.append(_render_mock_binaries_from_dir(tests_dir))
parts.append(_render_python_module_mocks_from_dir(tests_dir))
parts.append('exec "${TASK_ENTRYPOINT[@]}"')
if use_tls_entrypoint:
parts.append(_render_tls_task_entrypoint_exec())
else:
parts.append('exec "${TASK_ENTRYPOINT[@]}"')
return "\n".join(p for p in parts if p)


Expand Down
78 changes: 76 additions & 2 deletions .github/scripts/test_tekton_tasks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,62 @@ apply_python_command_mocks_merge() {
done
}

# When mocks.yaml sets hostname on an http_json service, map that hostname to the
# mock bind address via PipelineRun podTemplate.hostAliases (Task CRDs cannot set this).
apply_mock_host_aliases_to_pipelinerun_spec() {
local tests_dir="$1"
local test_path="$2"
local task_name="$3"
local pipelinerun_json="$4"
local mocks_yaml="${tests_dir}/mocks.yaml"
local mock_host mock_bind pipeline_task

[[ -f "$mocks_yaml" ]] || { echo "$pipelinerun_json"; return 0; }

mock_host=$(yq -r '.services[0].hostname // ""' "$mocks_yaml")
mock_bind=$(yq -r '.services[0].bind // "127.0.0.1"' "$mocks_yaml")
if [[ -z "$mock_host" || "$mock_host" == "null" || "$mock_host" == "$mock_bind" ]]; then
echo "$pipelinerun_json"
return 0
fi

pipeline_task=$(
yq -r ".spec.tasks[] | select(.taskRef.name == \"${task_name}\") | .name" \
"$test_path"
)
if [[ -z "$pipeline_task" || "$pipeline_task" == "null" ]]; then
echo " Warning: mocks.yaml hostname set but no taskRef.name=${task_name} in ${test_path}" >&2
echo "$pipelinerun_json"
return 0
fi

echo " Applying hostAliases ${mock_host} -> ${mock_bind} for pipeline task ${pipeline_task}" >&2
jq \
--arg task "$pipeline_task" \
--arg ip "$mock_bind" \
--arg host "$mock_host" \
'.spec.taskRunSpecs = [{
pipelineTaskName: $task,
podTemplate: {
hostAliases: [{
ip: $ip,
hostnames: [$host]
}]
}
}]' <<<"$pipelinerun_json"
}

needs_mock_host_aliases() {
local tests_dir="$1"
local mocks_yaml="${tests_dir}/mocks.yaml"
local mock_host mock_bind

[[ -f "$mocks_yaml" ]] || return 1
mock_host=$(yq -r '.services[0].hostname // ""' "$mocks_yaml")
mock_bind=$(yq -r '.services[0].bind // "127.0.0.1"' "$mocks_yaml")
[[ -n "$mock_host" && "$mock_host" != "null" && "$mock_host" != "$mock_bind" ]]
}

show_help() {
echo "Usage: $0 [--remove-compute-resources] [--no-cleanup] [item1] [item2] [...]"
echo
Expand Down Expand Up @@ -338,8 +394,26 @@ do
sleep 5
done

PIPELINERUNJSON=$(tkn p start --use-param-defaults $TEST_NAME ${ociStorageParam} ${dataDirParam} -w "name=tests-workspace,${workSpaceParams}" -o json)
PIPELINERUN=$(jq -r '.metadata.name' <<< "${PIPELINERUNJSON}")
if needs_mock_host_aliases "$TESTS_DIR"; then
PIPELINERUNJSON=$(
tkn p start --dry-run --use-param-defaults "$TEST_NAME" \
${ociStorageParam} ${dataDirParam} \
-w "name=tests-workspace,${workSpaceParams}" -o json
)
PIPELINERUNJSON=$(
apply_mock_host_aliases_to_pipelinerun_spec \
"$TESTS_DIR" "$TEST_PATH" "$TASK_NAME" "$PIPELINERUNJSON"
)
CREATED_PRLINE=$(echo "$PIPELINERUNJSON" | kubectl create -f - -o json)
PIPELINERUN=$(jq -r '.metadata.name' <<< "${CREATED_PRLINE}")
else
PIPELINERUNJSON=$(
tkn p start --use-param-defaults "$TEST_NAME" \
${ociStorageParam} ${dataDirParam} \
-w "name=tests-workspace,${workSpaceParams}" -o json
)
PIPELINERUN=$(jq -r '.metadata.name' <<< "${PIPELINERUNJSON}")
fi

echo " Started pipelinerun $PIPELINERUN"
sleep 1 # allow a second for the pr object to appear (including a status condition)
Expand Down
96 changes: 13 additions & 83 deletions tasks/managed/close-advisory-issues/close-advisory-issues.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -121,96 +121,26 @@ spec:
- name: sourceDataArtifact
value: $(params.sourceDataArtifact)
- name: close-issues
image: quay.io/konflux-ci/release-service-utils@sha256:5546fa78d3c88d7b6a2e8cff8902f7757f00541d0bbaf113b9f293133894afa3
image: quay.io/jbieren/release-service-utils:closeissues_2
computeResources:
limits:
memory: 32Mi
memory: 128Mi
requests:
memory: 32Mi
memory: 128Mi
cpu: 100m
volumeMounts:
- name: access-token-secret-vol
mountPath: "/etc/secrets"
script: |
#!/usr/bin/env bash
# We do not set -x here because it would leak the ACCESS_TOKEN
set -eu

ACCESS_TOKEN="$(cat /etc/secrets/token)"
EMAIL="$(cat /etc/secrets/email)"

ISSUE_TRACKERS='{
"Jira": {
"api": "rest/api/2/issue",
"servers": [
"issues.redhat.com",
"jira.atlassian.com",
"redhat.atlassian.net"
]
},
"bugzilla": {
"api": "rest/bug",
"servers": [
"bugzilla.redhat.com"
]
}
}'

DATA_FILE="$(params.dataDir)/$(params.dataPath)"
if [ ! -f "${DATA_FILE}" ] ; then
echo "No data JSON was provided."
exit 1
fi

NUM_ISSUES=$(jq -cr '.releaseNotes.issues.fixed | length' "${DATA_FILE}")
for ((i = 0; i < NUM_ISSUES; i++)); do
issue=$(jq -c --argjson i "$i" '.releaseNotes.issues.fixed[$i]' "${DATA_FILE}")
server=$(jq -r '.source' <<< "$issue")
if [ "$server" = "issues.redhat.com" ] ; then
server=redhat.atlassian.net
fi
if [ "$server" != "redhat.atlassian.net" ] ; then
echo "This task currently only supports closing issues on issues.redhat.com"
echo "and redhat.atlassian.net. Skipping issue $issue as it is on $server"
continue
fi

CURL_ARGS=(-u "${EMAIL}:${ACCESS_TOKEN}")

API=$(jq -r '.[] | select(.servers[] | contains("'"$server"'")) | .api' <<< "$ISSUE_TRACKERS")
API_URL="https://${server}/${API}/$(jq -r '.id' <<< "$issue")"

if [ "$(curl-with-retry "${CURL_ARGS[@]}" "${API_URL}" | jq -r '.fields.status.name')" == "Closed" ] ; then
echo "Issue $issue is already in Closed state. Skipping it."
continue
fi

echo "Closing issue $issue"
CLOSE_COMMENT="Fixed in Konflux Advisory $(params.advisoryUrl)"
CLOSING_RC=0
# Get the Closed transition id. This varies per Jira project
if ! CLOSED_ID="$(curl-with-retry "${CURL_ARGS[@]}" "${API_URL}/transitions" \
| jq -er '.transitions[] | select(.name=="Closed") | .id')"; then
echo "Warning: failed to fetch the closed state id for issue $issue. We most likely do not have" \
"permission to close it. Will try to add a comment instead."
CLOSING_RC=1
# Close the issue
elif ! curl-with-retry "${CURL_ARGS[@]}" -XPOST --data \
'{"transition":{"id":"'"$CLOSED_ID"'"},"update":{"comment":[{"add":{"body":"'"$CLOSE_COMMENT"'"}}]}}' \
-H "Content-Type: application/json" "${API_URL}/transitions"; then
echo "Warning: failed to close issue $issue. Will try to add a comment instead."
CLOSING_RC=1
fi

if [ "$CLOSING_RC" -ne 0 ] ; then
# Try to add the comment even if closing failed
if ! curl-with-retry "${CURL_ARGS[@]}" -XPOST --data \
'{"body":"'"$CLOSE_COMMENT"'"}' \
-H "Content-Type: application/json" "${API_URL}/comment"; then
echo "Warning: failed to add comment to issue $issue."
fi
fi
done
env:
- name: PARAM_DATA_DIR
value: $(params.dataDir)
- name: PARAM_DATA_PATH
value: $(params.dataPath)
- name: PARAM_ADVISORY_URL
value: $(params.advisoryUrl)
- name: JIRA_SECRET_PATH
value: /etc/secrets
command: ["/home/scripts/python/tasks/managed/close_advisory_issues.py"]
- name: create-trusted-artifact
computeResources:
limits:
Expand Down
37 changes: 0 additions & 37 deletions tasks/managed/close-advisory-issues/tests/mocks.sh

This file was deleted.

20 changes: 20 additions & 0 deletions tasks/managed/close-advisory-issues/tests/mocks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
# Mock Jira on localhost; test_tekton_tasks.sh applies hostAliases on the PipelineRun
# when hostname is set below. pre-apply-task-hook.sh adds NET_BIND_SERVICE for port 443.
version: 1
services:
- type: http_json
bind: 127.0.0.1
port: 443
tls: true
hostname: redhat.atlassian.net
routes:
- path_contains: /issue/ISSUE-123/transitions
method: POST
body: '{}'
- path_contains: /issue/ISSUE-123/transitions
method: GET
body: '{"transitions":[{"id":"91","name":"Closed","description":""},{"id":"11","name":"New"}]}'
- path_contains: /issue/ISSUE-123
method: GET
body: '{"fields":{"status":{"name":"Open"}}}'
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail

# Add mocks to the beginning of task step script
TASK_PATH="$1"
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
yq -i '.spec.steps[1].script = load_str("'$SCRIPT_DIR'/mocks.sh") + .spec.steps[1].script' "$TASK_PATH"

# Bind port 443 for the in-pod mock without running as root (test task copy only).
yq -i \
'.spec.steps[1].securityContext.capabilities = {"add": ["NET_BIND_SERVICE"]}' \
"$TASK_PATH"

# Create a dummy access token secret (and delete it first if it exists)
kubectl delete secret konflux-advisory-jira-secret --ignore-not-found
kubectl create secret generic konflux-advisory-jira-secret --from-literal=token=abcdefg --from-literal=email=team@domain.com
kubectl create secret generic konflux-advisory-jira-secret \
--from-literal=token=abcdefg \
--from-literal=email=team@domain.com
Loading
Loading