feat: add preview orphan reconciler
This commit is contained in:
parent
3386982cfc
commit
b431f5cadb
|
|
@ -2,3 +2,4 @@ apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
resources:
|
resources:
|
||||||
- maidn-node-static-image.yaml
|
- maidn-node-static-image.yaml
|
||||||
|
- maidn-preview-orphan-reconciler.yaml
|
||||||
|
|
|
||||||
276
catalog/maidn-preview-orphan-reconciler.yaml
Normal file
276
catalog/maidn-preview-orphan-reconciler.yaml
Normal file
|
|
@ -0,0 +1,276 @@
|
||||||
|
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"
|
||||||
115
catalog/test-preview-orphan-reconciler.sh
Normal file
115
catalog/test-preview-orphan-reconciler.sh
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
task="$root/catalog/maidn-preview-orphan-reconciler.yaml"
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
|
||||||
|
! grep -q 'secretKeyRef' "$task"
|
||||||
|
grep -qF 'key: forgejo-origin' "$task"
|
||||||
|
awk '
|
||||||
|
/ - name: reconcile/ { found=1 }
|
||||||
|
found && /^ script: \|/ { script=1; next }
|
||||||
|
script && /^ - name:/ { exit }
|
||||||
|
script { sub(/^ /, ""); print }
|
||||||
|
' "$task" > "$tmp/reconcile.sh"
|
||||||
|
|
||||||
|
mkdir "$tmp/bin" "$tmp/fixture"
|
||||||
|
cat > "$tmp/bin/git" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
case "$1" in
|
||||||
|
clone)
|
||||||
|
for arg; do target=$arg; done
|
||||||
|
cp -R "$TEST_FIXTURE" "$target"
|
||||||
|
;;
|
||||||
|
-C)
|
||||||
|
shift 2
|
||||||
|
case "$1" in
|
||||||
|
remote)
|
||||||
|
[ "$2" = get-url ] && [ "$3" = origin ] || exit 1
|
||||||
|
printf '%s\n' "$TEST_ORIGIN"
|
||||||
|
;;
|
||||||
|
rev-parse|config|add|push) exit 0 ;;
|
||||||
|
diff) exit 1 ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
diff) exit 1 ;;
|
||||||
|
commit)
|
||||||
|
[ ! -e "$PWD/apps/previews/web-ui-pr-42" ]
|
||||||
|
! grep -qF ' - web-ui-pr-42' "$PWD/apps/previews/kustomization.yaml"
|
||||||
|
;;
|
||||||
|
config|add|push) exit 0 ;;
|
||||||
|
*) exit 1 ;;
|
||||||
|
esac
|
||||||
|
EOF
|
||||||
|
chmod +x "$tmp/bin/git"
|
||||||
|
|
||||||
|
marker() {
|
||||||
|
cat <<'EOF'
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: maidn-preview-owner
|
||||||
|
namespace: web-ui-pr-42
|
||||||
|
labels:
|
||||||
|
maidn.io/preview-owner: "true"
|
||||||
|
maidn.io/preview-app: "web-ui"
|
||||||
|
maidn.io/preview-pr: "42"
|
||||||
|
annotations:
|
||||||
|
maidn.io/preview-repository: "platform/web-ui"
|
||||||
|
data:
|
||||||
|
app: "web-ui"
|
||||||
|
repository: "platform/web-ui"
|
||||||
|
pr-number: "42"
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
fixture() {
|
||||||
|
dir=$1
|
||||||
|
mkdir -p "$dir/apps/previews/web-ui-pr-42"
|
||||||
|
cat > "$dir/apps/previews/kustomization.yaml" <<'EOF'
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- web-ui-pr-42
|
||||||
|
EOF
|
||||||
|
marker > "$dir/apps/previews/web-ui-pr-42/ownership.yaml"
|
||||||
|
: > "$dir/apps/previews/web-ui-pr-42/namespace.yaml"
|
||||||
|
: > "$dir/apps/previews/web-ui-pr-42/values.yaml"
|
||||||
|
: > "$dir/apps/previews/web-ui-pr-42/kustomization.yaml"
|
||||||
|
: > "$dir/apps/previews/web-ui-pr-42/release.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
TRUSTED_FORGEJO_ORIGIN=https://git.example.invalid \
|
||||||
|
TRUSTED_MANIFESTS_URL=https://git.example.invalid/platform/manifests.git \
|
||||||
|
TRUSTED_MANIFESTS_BRANCH=main APP_NAME="$1" APP_REPOSITORY=platform/web-ui PR_NUMBER=42 \
|
||||||
|
TEST_FIXTURE="$tmp/fixture" PATH="$tmp/bin:$PATH" \
|
||||||
|
TEST_ORIGIN="${TEST_ORIGIN:-https://git.example.invalid/platform/manifests.git}" \
|
||||||
|
sh "$tmp/reconcile.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
fixture "$tmp/fixture"
|
||||||
|
run web-ui
|
||||||
|
[ -f "$tmp/fixture/apps/previews/web-ui-pr-42/ownership.yaml" ]
|
||||||
|
grep -qF ' - web-ui-pr-42' "$tmp/fixture/apps/previews/kustomization.yaml"
|
||||||
|
|
||||||
|
rm -rf "$tmp/fixture"
|
||||||
|
fixture "$tmp/fixture"
|
||||||
|
printf '\n' >> "$tmp/fixture/apps/previews/web-ui-pr-42/ownership.yaml"
|
||||||
|
if run web-ui; then exit 1; fi
|
||||||
|
[ -f "$tmp/fixture/apps/previews/web-ui-pr-42/ownership.yaml" ]
|
||||||
|
|
||||||
|
rm -rf "$tmp/fixture"
|
||||||
|
fixture "$tmp/fixture"
|
||||||
|
rm "$tmp/fixture/apps/previews/web-ui-pr-42/ownership.yaml"
|
||||||
|
ln -s /tmp/not-owned "$tmp/fixture/apps/previews/web-ui-pr-42/ownership.yaml"
|
||||||
|
if run web-ui; then exit 1; fi
|
||||||
|
|
||||||
|
rm -rf "$tmp/fixture"
|
||||||
|
fixture "$tmp/fixture"
|
||||||
|
TEST_ORIGIN=https://git.example.invalid/platform/other.git
|
||||||
|
if run web-ui; then exit 1; fi
|
||||||
|
if run 'web-ui/escape'; then exit 1; fi
|
||||||
Loading…
Reference in a new issue