tekton-pipelines/catalog/maidn-preview-orphan-reconciler.yaml

277 lines
11 KiB
YAML

apiVersion: tekton.dev/v1
kind: Task
metadata:
name: maidn-preview-orphan-reconciler
namespace: tekton-pipelines
annotations:
# The environment GitOps repo owns this ConfigMap; render it before enabling this Task.
maidn.io/delivery-config: maidn-preview-delivery-config
# The TaskRun service account supplies this annotated Git Secret through Tekton's initializer.
maidn.io/git-credentials: forgejo-git-credentials
spec:
stepTemplate:
env:
- name: HOME
value: /tekton/home
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
params:
- name: app-name
type: string
- name: app-repository
type: string
- name: pr-number
type: string
steps:
- name: verify-closed-pr
image: python:3.13-alpine3.21
env:
- name: TRUSTED_FORGEJO_ORIGIN
valueFrom:
configMapKeyRef:
name: maidn-preview-delivery-config
key: forgejo-origin
- name: TRUSTED_MANIFESTS_URL
valueFrom:
configMapKeyRef:
name: maidn-preview-delivery-config
key: manifests-url
- name: TRUSTED_MANIFESTS_BRANCH
valueFrom:
configMapKeyRef:
name: maidn-preview-delivery-config
key: manifests-branch
- name: APP_NAME
value: $(params.app-name)
- name: APP_REPOSITORY
value: $(params.app-repository)
- name: PR_NUMBER
value: $(params.pr-number)
script: |
#!/bin/sh
set -eu
fail() { exit 1; }
valid_forgejo_origin() {
case "$1" in https://*) ;; *) fail ;; esac
host=${1#https://}
case "$host" in ''|.*|*..*|*.) fail ;; esac
case "$host" in *[!A-Za-z0-9.-]*) fail ;; esac
}
valid_git_url() {
case "$1" in https://*/*.git) ;; *) fail ;; esac
repository=${1#https://}
host=${repository%%/*}
path=${repository#*/}
case "$host" in ''|.*|*..*|*.) fail ;; esac
case "$host" in *[!A-Za-z0-9.-]*) fail ;; esac
case "$path" in ''|*[!A-Za-z0-9._/-]*|/*|*//*|*..*) fail ;; esac
[ "https://$host" = "$TRUSTED_FORGEJO_ORIGIN" ] || fail
}
valid_branch() {
case "$1" in [A-Za-z0-9]*) ;; *) fail ;; esac
case "$1" in *[!A-Za-z0-9._/-]*|*..*|*//*|*/) fail ;; esac
}
valid_name() {
case "$1" in ''|*[!a-z0-9-]*|-*|*-) fail ;; esac
[ "${#1}" -le 63 ] || fail
}
valid_repository() {
case "$1" in */*) ;; *) fail ;; esac
owner=${1%%/*}
repository=${1#*/}
[ "$owner/$repository" = "$1" ] || fail
case "$owner" in ''|.*|*.|*[!A-Za-z0-9._-]*|*..*) fail ;; esac
case "$repository" in ''|.*|*.|*[!A-Za-z0-9._-]*|*..*) fail ;; esac
}
valid_pr_number() {
case "$1" in [1-9]*) ;; *) fail ;; esac
case "$1" in *[!0-9]*) fail ;; esac
[ $((${#APP_NAME} + ${#1} + 4)) -le 63 ] || fail
}
valid_forgejo_origin "$TRUSTED_FORGEJO_ORIGIN"
valid_git_url "$TRUSTED_MANIFESTS_URL"
valid_branch "$TRUSTED_MANIFESTS_BRANCH"
valid_name "$APP_NAME"
valid_repository "$APP_REPOSITORY"
valid_pr_number "$PR_NUMBER"
python3 - <<'PY'
import base64
import json
import os
import sys
from pathlib import Path
from urllib.error import URLError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener
origin = urlsplit(os.environ["TRUSTED_FORGEJO_ORIGIN"])
try:
lines = Path(os.environ["HOME"], ".git-credentials").read_text().splitlines()
matches = []
for line in lines:
credential = urlsplit(line)
if (
credential.scheme == "https"
and credential.hostname == origin.hostname
and credential.path in ("", "/")
and not credential.query
and not credential.fragment
and credential.username
and credential.password
):
matches.append((credential.username, credential.password))
except (OSError, ValueError):
sys.exit(1)
if len(matches) != 1:
sys.exit(1)
repository = os.environ["APP_REPOSITORY"]
number = int(os.environ["PR_NUMBER"])
credentials = "%s:%s" % matches[0]
request = Request("%s/api/v1/repos/%s/pulls/%s" % (os.environ["TRUSTED_FORGEJO_ORIGIN"], repository, number))
request.add_header("Authorization", "Basic " + base64.b64encode(credentials.encode()).decode())
request.add_header("Accept", "application/json")
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, request, fp, code, msg, headers, url):
return None
try:
with build_opener(NoRedirect).open(request, timeout=20) as response:
pull = json.load(response)
except (URLError, ValueError, json.JSONDecodeError):
sys.exit(1)
if type(pull) is not dict or type(pull.get("number")) is not int or pull["number"] != number or pull.get("state") != "closed":
sys.exit(1)
PY
- name: reconcile
image: alpine/git:2.47.2
env:
- name: TRUSTED_FORGEJO_ORIGIN
valueFrom:
configMapKeyRef:
name: maidn-preview-delivery-config
key: forgejo-origin
- name: TRUSTED_MANIFESTS_URL
valueFrom:
configMapKeyRef:
name: maidn-preview-delivery-config
key: manifests-url
- name: TRUSTED_MANIFESTS_BRANCH
valueFrom:
configMapKeyRef:
name: maidn-preview-delivery-config
key: manifests-branch
- name: APP_NAME
value: $(params.app-name)
- name: APP_REPOSITORY
value: $(params.app-repository)
- name: PR_NUMBER
value: $(params.pr-number)
script: |
#!/bin/sh
set -eu
fail() { exit 1; }
valid_forgejo_origin() {
case "$1" in https://*) ;; *) fail ;; esac
host=${1#https://}
case "$host" in ''|.*|*..*|*.) fail ;; esac
case "$host" in *[!A-Za-z0-9.-]*) fail ;; esac
}
valid_git_url() {
case "$1" in https://*/*.git) ;; *) fail ;; esac
repository=${1#https://}
host=${repository%%/*}
path=${repository#*/}
case "$host" in ''|.*|*..*|*.) fail ;; esac
case "$host" in *[!A-Za-z0-9.-]*) fail ;; esac
case "$path" in ''|*[!A-Za-z0-9._/-]*|/*|*//*|*..*) fail ;; esac
[ "https://$host" = "$TRUSTED_FORGEJO_ORIGIN" ] || fail
}
valid_branch() {
case "$1" in [A-Za-z0-9]*) ;; *) fail ;; esac
case "$1" in *[!A-Za-z0-9._/-]*|*..*|*//*|*/) fail ;; esac
}
valid_name() {
case "$1" in ''|*[!a-z0-9-]*|-*|*-) fail ;; esac
[ "${#1}" -le 63 ] || fail
}
valid_repository() {
case "$1" in */*) ;; *) fail ;; esac
owner=${1%%/*}
repository=${1#*/}
[ "$owner/$repository" = "$1" ] || fail
case "$owner" in ''|.*|*.|*[!A-Za-z0-9._-]*|*..*) fail ;; esac
case "$repository" in ''|.*|*.|*[!A-Za-z0-9._-]*|*..*) fail ;; esac
}
valid_pr_number() {
case "$1" in [1-9]*) ;; *) fail ;; esac
case "$1" in *[!0-9]*) fail ;; esac
[ $((${#APP_NAME} + ${#1} + 4)) -le 63 ] || fail
}
valid_forgejo_origin "$TRUSTED_FORGEJO_ORIGIN"
valid_git_url "$TRUSTED_MANIFESTS_URL"
valid_branch "$TRUSTED_MANIFESTS_BRANCH"
valid_name "$APP_NAME"
valid_repository "$APP_REPOSITORY"
valid_pr_number "$PR_NUMBER"
namespace="$APP_NAME-pr-$PR_NUMBER"
preview_path="apps/previews/$namespace"
umask 077
private_dir=$(mktemp -d)
chmod 700 "$private_dir"
trap 'rm -rf -- "$private_dir"' EXIT HUP INT TERM
repository_path="$private_dir/manifests"
git clone --branch "$TRUSTED_MANIFESTS_BRANCH" "$TRUSTED_MANIFESTS_URL" "$repository_path"
[ "$(git -C "$repository_path" remote get-url origin)" = "$TRUSTED_MANIFESTS_URL" ] || fail
git -C "$repository_path" rev-parse --verify "refs/heads/$TRUSTED_MANIFESTS_BRANCH" >/dev/null
cd "$repository_path"
[ -d apps ] && [ ! -L apps ] || fail
[ -d apps/previews ] && [ ! -L apps/previews ] || fail
[ -d "$preview_path" ] && [ ! -L "$preview_path" ] || fail
marker="$preview_path/ownership.yaml"
[ -f "$marker" ] && [ ! -L "$marker" ] || fail
for existing in "$preview_path"/* "$preview_path"/.[!.]* "$preview_path"/..?*; do
[ -e "$existing" ] || [ -L "$existing" ] || continue
[ -f "$existing" ] && [ ! -L "$existing" ] || fail
case "${existing##*/}" in namespace.yaml|ownership.yaml|values.yaml|kustomization.yaml|release.yaml) ;; *) fail ;; esac
done
expected_marker=$(mktemp "$private_dir/expected-marker.XXXXXX")
cat > "$expected_marker" <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: maidn-preview-owner
namespace: $namespace
labels:
maidn.io/preview-owner: "true"
maidn.io/preview-app: "$APP_NAME"
maidn.io/preview-pr: "$PR_NUMBER"
annotations:
maidn.io/preview-repository: "$APP_REPOSITORY"
data:
app: "$APP_NAME"
repository: "$APP_REPOSITORY"
pr-number: "$PR_NUMBER"
EOF
cmp -s "$expected_marker" "$marker" || fail
root="apps/previews/kustomization.yaml"
[ -f "$root" ] && [ ! -L "$root" ] || fail
grep -qxF 'kind: Kustomization' "$root" || fail
grep -qxF 'resources:' "$root" || fail
[ "$(grep -cxF " - $namespace" "$root")" -eq 1 ] || fail
rm -rf -- "$preview_path"
sed -i "\|^ - $namespace$|d" "$root"
git add -u -- "$preview_path" "$root"
git diff --cached --quiet && fail
git config --local user.name Maidn
git config --local user.email maidn@free-maidn.com
git commit -m "chore: remove closed preview $namespace"
# HOME is /tekton/home, where Tekton's annotated-secret initializer configures git.
git push origin "$TRUSTED_MANIFESTS_BRANCH"