Skip to content

Commit

Permalink
chore(RELEASE-1039): add create-advisory internal task and pipeline (k…
Browse files Browse the repository at this point in the history
…onflux-ci#710)

This commit moves the create-advisory pipeline and task from the
app-interface repo to the internal directory of this repo. It
also adds tests and a README with it.

Also resolves RELEASE-1097

Signed-off-by: Johnny Bieren <[email protected]>
  • Loading branch information
johnbieren authored Nov 26, 2024
1 parent c18297b commit fe8d948
Show file tree
Hide file tree
Showing 10 changed files with 469 additions and 0 deletions.
16 changes: 16 additions & 0 deletions internal/pipelines/create-advisory/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# create-advisory pipeline

Tekton pipeline to execute the create-advisory task. The task clones the advisory repo, gets an ID for the advisory,
generates an advisory, then pushes it if the generated advisory is valid. The pipeline returns a result with the
advisory URL as well as a result to show the error message if one occurred.

## Parameters

| Name | Description | Optional | Default value |
|----------------------|--------------------------------------------------------------------------------------------------------|----------|---------------|
| advisory_json | String containing a JSON representation of the advisory data (e.g. '{"product_id":123,"type":"RHSA"}') | No | - |
| application | Application being released | No | - |
| origin | The origin workspace where the release CR comes from. This is used to determine the advisory path | No | - |
| config_map_name | The name of the configMap that contains the signing key | No | - |
| advisory_secret_name | The name of the secret that contains the advisory creation metadata | No | - |
| errata_secret_name | The name of the secret that contains the errata service account metadata | No | - |
57 changes: 57 additions & 0 deletions internal/pipelines/create-advisory/create-advisory.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: create-advisory
labels:
app.kubernetes.io/version: "0.5.0"
annotations:
tekton.dev/pipelines.minVersion: "0.12.1"
tekton.dev/tags: advisory
spec:
description: >-
Pipeline to push an advisory yaml to a Git repository
params:
- name: advisory_json
type: string
description: |
String containing a JSON representation of the advisory data (e.g. '{"product_id":123,"type":"RHSA"}')
- name: application
type: string
description: Application being released
- name: origin
type: string
description: |
The origin workspace where the release CR comes from.
This is used to determine the advisory path
- name: config_map_name
type: string
description: The name of the configMap that contains the signing key
- name: advisory_secret_name
type: string
description: The name of the secret that contains the advisory creation metadata
- name: errata_secret_name
type: string
description: The name of the secret that contains the errata service account metadata
tasks:
- name: create-advisory-task
taskRef:
name: create-advisory-task
params:
- name: advisory_json
value: $(params.advisory_json)
- name: application
value: $(params.application)
- name: origin
value: $(params.origin)
- name: config_map_name
value: $(params.config_map_name)
- name: advisory_secret_name
value: $(params.advisory_secret_name)
- name: errata_secret_name
value: $(params.errata_secret_name)
results:
- name: result
value: $(tasks.create-advisory-task.results.result)
- name: advisory_url
value: $(tasks.create-advisory-task.results.advisory_url)
1 change: 1 addition & 0 deletions internal/resources/create-advisory-task.yaml
1 change: 1 addition & 0 deletions internal/resources/create-advisory.yaml
16 changes: 16 additions & 0 deletions internal/tasks/create-advisory-task/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# create-advisory-task

Pushes an advisory yaml to a Git repository. The task will always exit 0 even if something fails. This is because the task result
will not be set if the task fails, and the task result should always be set and propagated back to the cluster that creates the
internal request. The success/failure is handled in the task creating the internal request.

## Parameters

| Name | Description | Optional | Default value |
|----------------------|--------------------------------------------------------------------------------------------------------|----------|---------------|
| advisory_json | String containing a JSON representation of the advisory data (e.g. '{"product_id":123,"type":"RHSA"}') | No | - |
| application | Application being released | No | - |
| origin | The origin workspace where the release CR comes from. This is used to determine the advisory path | No | - |
| config_map_name | The name of the configMap that contains the signing key | No | - |
| advisory_secret_name | The name of the secret that contains the advisory creation metadata | No | - |
| errata_secret_name | The name of the secret that contains the errata service account metadata | No | - |
177 changes: 177 additions & 0 deletions internal/tasks/create-advisory-task/create-advisory-task.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
---
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: create-advisory-task
labels:
app.kubernetes.io/version: "0.11.0"
annotations:
tekton.dev/pipelines.minVersion: "0.12.1"
tekton.dev/tags: release
spec:
description: |
Pushes an advisory yaml to a Git repository.
The task will always exit 0 even if something fails. This is because the task result will not be
set if the task fails, and the task result should always be set and propagated back to the cluster
that creates the internal request. The success/failure is handled in the task creating the internal
request.
params:
- name: advisory_json
type: string
description: |
String containing a JSON representation of the advisory data (e.g. '{"product_id":123,"type":"RHSA"}').
- name: application
type: string
description: Application being released
- name: origin
type: string
description: |
The origin workspace where the release CR comes from.
This is used to determine the advisory path
- name: config_map_name
type: string
description: The name of the configMap that contains the signing key
- name: advisory_secret_name
type: string
description: The name of the secret that contains the advisory creation metadata
- name: errata_secret_name
type: string
description: The name of the secret that contains the errata service account metadata
results:
- name: result
description: Success if the task succeeds, the error otherwise
- name: advisory_url
description: The advisory url if the task succeeds, empty string otherwise
steps:
- name: create-advisory
image: quay.io/konflux-ci/release-service-utils:9089cafbf36bb889b4b73d8c2965613810f13736
env:
- name: GITLAB_HOST
valueFrom:
secretKeyRef:
name: $(params.advisory_secret_name)
key: gitlab_host
# This is a GitLab Project access token. Go to the settings/access_tokens page
# of your repository to create one. It should have the Developer role with read
# and write repository rights.
- name: ACCESS_TOKEN
valueFrom:
secretKeyRef:
name: $(params.advisory_secret_name)
key: gitlab_access_token
- name: GIT_AUTHOR_NAME
valueFrom:
secretKeyRef:
name: $(params.advisory_secret_name)
key: git_author_name
- name: GIT_AUTHOR_EMAIL
valueFrom:
secretKeyRef:
name: $(params.advisory_secret_name)
key: git_author_email
- name: GIT_REPO
valueFrom:
secretKeyRef:
name: $(params.advisory_secret_name)
key: git_repo
- name: ERRATA_API
valueFrom:
secretKeyRef:
name: $(params.errata_secret_name)
key: errata_api
- name: SERVICE_ACCOUNT_NAME
valueFrom:
secretKeyRef:
name: $(params.errata_secret_name)
key: name
- name: SERVICE_ACCOUNT_KEYTAB
valueFrom:
secretKeyRef:
name: $(params.errata_secret_name)
key: base64_keytab
- name: "ADVISORY_JSON"
value: "$(params.advisory_json)"
script: |
#!/usr/bin/env bash
set -eo pipefail
exitfunc() {
local err=$1
local line=$2
local command="$3"
if [ "$err" -eq 0 ] ; then
echo -n "Success" > "$(results.result.path)"
else
echo -n \
"$0: ERROR '$command' failed at line $line - exited with status $err" > "$(results.result.path)"
fi
echo -n "${ADVISORY_URL}" > "$(results.advisory_url.path)"
exit 0 # exit the script cleanly as there is no point in proceeding past an error or exit call
}
# due to set -e, this catches all EXIT and ERR calls and the task should never fail with nonzero exit code
trap 'exitfunc $? $LINENO "$BASH_COMMAND"' EXIT
REPO_BRANCH=main
ADVISORY_URL=""
# Switch to /tmp to avoid filesystem permission issues
cd /tmp
# loading git and gitlab functions
# shellcheck source=/dev/null
. /home/utils/gitlab-functions
# shellcheck source=/dev/null
. /home/utils/git-functions
gitlab_init
git_functions_init
# This also cds into the git repo
git_clone_and_checkout --repository "$GIT_REPO" --revision "$REPO_BRANCH"
# Inject signing key into ADVISORY_JSON
signingKey=$(kubectl get configmap "$(params.config_map_name)" -o jsonpath="{.data.SIG_KEY_NAME}")
advisoryJsonWithKey=$(jq -c --arg key "$signingKey" \
'.content.images[] += {"signingKey": $key}' <<< "$ADVISORY_JSON")
# write keytab to file
echo -n "${SERVICE_ACCOUNT_KEYTAB}" | base64 --decode > /tmp/keytab
# workaround kinit: Invalid UID in persistent keyring name while getting default ccache
KRB5CCNAME=$(mktemp)
export KRB5CCNAME
# see https://stackoverflow.com/a/12308187
KRB5_CONFIG=$(mktemp)
export KRB5_CONFIG
export KRB5_TRACE=/dev/stderr
sed '/\[libdefaults\]/a\ dns_canonicalize_hostname = false' /etc/krb5.conf > "${KRB5_CONFIG}"
kinit "${SERVICE_ACCOUNT_NAME}" -k -t /tmp/keytab
ID=$(curl --retry 3 --negotiate -u : "${ERRATA_API}/advisory/reserve_live_id" -XPOST | jq -r '.live_id')
ADVISORY_NUM=$(printf "%04d" "$ID")
# Use ISO 8601 format in UTC/Zulu time, e.g. 2024-03-06T17:27:38Z
SHIP_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
YEAR=${SHIP_DATE%%-*} # derive the year from the ship date
# group advisories by <origin workspace>/year
ADVISORY_DIR="data/advisories/$(params.origin)/${YEAR}/${ADVISORY_NUM}"
mkdir -p "${ADVISORY_DIR}"
ADVISORY_FILEPATH="${ADVISORY_DIR}/advisory.yaml"
ADVISORY_NAME="${YEAR}:${ADVISORY_NUM}"
# Prepare variables for the advisory template
DATA=$(jq -c '{"advisory":{"spec":.}}' <<< "$advisoryJsonWithKey")
DATA=$(jq -c --arg advisory_name "$ADVISORY_NAME" --arg advisory_ship_date "$SHIP_DATE" \
'$ARGS.named + .' <<< "$DATA")
# Create advisory file
/home/utils/apply_template.py -o "$ADVISORY_FILEPATH" --data "$DATA" \
--template /home/templates/advisory.yaml.jinja
# Ensure the created advisory file passes the advisory schema
check-jsonschema --schemafile schema/advisory.json "$ADVISORY_FILEPATH"
git add "${ADVISORY_FILEPATH}"
git commit -m "[Konflux Release] new advisory for $(params.application)"
echo "Pushing to ${REPO_BRANCH}..."
git_push_with_retries --branch $REPO_BRANCH --retries 5 --url origin
# Construct the advisory url to report back to the user as a result
# Note: This currently only supports gitlab repos, which is all we expect for now
ADVISORY_URL="${GIT_REPO//\.git/}/-/blob/${REPO_BRANCH}/${ADVISORY_FILEPATH}"
66 changes: 66 additions & 0 deletions internal/tasks/create-advisory-task/tests/mocks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -eux

# mocks to be injected into task step scripts
function git() {
echo "Mock git called with: $*"

if [[ "$*" == *"clone"* ]]; then
gitRepo=$(echo "$*" | cut -f5 -d/ | cut -f1 -d.)
mkdir -p "$gitRepo"/schema
echo '{"$schema": "http://json-schema.org/draft-07/schema#","type": "object", "properties":{}}' > "$gitRepo"/schema/advisory.json
elif [[ "$*" == *"failing-tenant"* ]]; then
echo "Mocking failing git command" && false
else
# Mock the other git functions to pass
: # no-op - do nothing
fi
}

function glab() {
echo "Mock glab called with: $*"

if [[ "$*" != "auth login"* ]]; then
echo Error: Unexpected call
exit 1
fi
}

function kinit() {
echo "kinit $*"
}

function curl() {
echo Mock curl called with: $* >&2

if [[ "$*" == "--retry 3 --negotiate -u : https://errata/api/v1/advisory/reserve_live_id -XPOST" ]] ; then
echo '{"live_id": 1234}'
else
echo Error: Unexpected call
exit 1
fi
}

function date() {
echo Mock date called with: $* >&2

case "$*" in
*"+%Y-%m-%dT%H:%M:%SZ")
echo "2024-12-12T00:00:00Z"
;;
"*")
echo Error: Unexpected call
exit 1
;;
esac
}

function kubectl() {
# The default SA doesn't have perms to get configmaps, so mock the `kubectl get configmap` call
if [[ "$*" == "get configmap create-advisory-test-cm -o jsonpath={.data.SIG_KEY_NAME}" ]]
then
echo key1
else
/usr/bin/kubectl $*
fi
}
13 changes: 13 additions & 0 deletions internal/tasks/create-advisory-task/tests/pre-apply-task-hook.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash

TASK_PATH="$1"
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )

# Add mocks to the beginning of task step script
yq -i '.spec.steps[0].script = load_str("'$SCRIPT_DIR'/mocks.sh") + .spec.steps[0].script' "$TASK_PATH"

kubectl delete secret create-advisory-secret --ignore-not-found
kubectl create secret generic create-advisory-secret --from-literal=git_author_email=tester@tester --from-literal=git_author_name=tester --from-literal=gitlab_access_token=abc --from-literal=gitlab_host=myurl --from-literal=git_repo=https://gitlab.com/org/repo.git

kubectl delete secret create-advisory-errata-secret --ignore-not-found
kubectl create secret generic create-advisory-errata-secret --from-literal=errata_api=https://errata/api/v1 --from-literal=name=errata-tester --from-literal=base64_keytab=Zm9vCg==
Loading

0 comments on commit fe8d948

Please sign in to comment.