878 lines
44 KiB
Bash
878 lines
44 KiB
Bash
#!/bin/sh
|
|
set -u
|
|
|
|
tmp="$(mktemp -d)"
|
|
export tmp
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
|
|
json_tail() {
|
|
name="$1"
|
|
limit="${2:-2000}"
|
|
python3 - "$tmp/$name" "$limit" <<'PY'
|
|
import json, sys
|
|
path, limit = sys.argv[1], int(sys.argv[2])
|
|
try:
|
|
data = open(path, encoding="utf-8", errors="replace").read()[-limit:]
|
|
except FileNotFoundError:
|
|
data = ""
|
|
print(json.dumps(data))
|
|
PY
|
|
}
|
|
|
|
capture_json() {
|
|
name="$1"
|
|
shift
|
|
"$@" -o json >"$tmp/$name.json" 2>"$tmp/$name.err"
|
|
rc=$?
|
|
printf '%s' "$rc" >"$tmp/$name.rc"
|
|
}
|
|
|
|
run_apply() {
|
|
manifest="$tmp/gitea.k8s.yaml"
|
|
printf '%s' "$UNIDESK_GITEA_MANIFEST_B64" | base64 -d >"$manifest"
|
|
if [ "$UNIDESK_GITEA_FRPC_ENABLED" = "1" ] && [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then
|
|
printf '%s' "$UNIDESK_GITEA_FRPC_TOML_B64" | base64 -d >"$tmp/frpc.toml"
|
|
kubectl -n "$UNIDESK_GITEA_NAMESPACE" create secret generic "$UNIDESK_GITEA_FRPC_SECRET_NAME" \
|
|
--from-file="$UNIDESK_GITEA_FRPC_SECRET_KEY=$tmp/frpc.toml" \
|
|
--dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_GITEA_FIELD_MANAGER" -f - >"$tmp/frpc-secret.out" 2>"$tmp/frpc-secret.err"
|
|
frpc_rc=$?
|
|
else
|
|
frpc_rc=0
|
|
: >"$tmp/frpc-secret.out"
|
|
if [ "$UNIDESK_GITEA_FRPC_ENABLED" = "1" ]; then
|
|
printf '%s\n' "dry-run: frpc secret apply skipped" >"$tmp/frpc-secret.err"
|
|
else
|
|
printf '%s\n' "frpc disabled" >"$tmp/frpc-secret.err"
|
|
fi
|
|
fi
|
|
if [ "$UNIDESK_GITEA_WEBHOOK_SYNC_ENABLED" = "1" ] && [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then
|
|
kubectl -n "$UNIDESK_GITEA_NAMESPACE" create secret generic "$UNIDESK_GITEA_WEBHOOK_SECRET_NAME" \
|
|
--from-literal=github-token="$UNIDESK_GITEA_GITHUB_TOKEN" \
|
|
--from-literal=gitea-username="$UNIDESK_GITEA_ADMIN_USERNAME" \
|
|
--from-literal=gitea-password="$UNIDESK_GITEA_ADMIN_PASSWORD" \
|
|
--from-literal=github-webhook-secret="$UNIDESK_GITEA_GITHUB_WEBHOOK_SECRET" \
|
|
--dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_GITEA_FIELD_MANAGER" -f - >"$tmp/webhook-secret.out" 2>"$tmp/webhook-secret.err"
|
|
webhook_secret_rc=$?
|
|
else
|
|
webhook_secret_rc=0
|
|
: >"$tmp/webhook-secret.out"
|
|
if [ "$UNIDESK_GITEA_WEBHOOK_SYNC_ENABLED" = "1" ]; then
|
|
printf '%s\n' "dry-run: webhook sync secret apply skipped" >"$tmp/webhook-secret.err"
|
|
else
|
|
printf '%s\n' "webhook sync disabled" >"$tmp/webhook-secret.err"
|
|
fi
|
|
fi
|
|
dry_arg=""
|
|
if [ "$UNIDESK_GITEA_DRY_RUN" = "1" ]; then
|
|
dry_arg="--dry-run=server"
|
|
fi
|
|
if [ "$frpc_rc" -eq 0 ] && [ "$webhook_secret_rc" -eq 0 ]; then
|
|
timeout "$UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS" kubectl apply --server-side $dry_arg --force-conflicts --field-manager="$UNIDESK_GITEA_FIELD_MANAGER" -f "$manifest" >"$tmp/apply.out" 2>"$tmp/apply.err"
|
|
apply_rc=$?
|
|
else
|
|
apply_rc=1
|
|
: >"$tmp/apply.out"
|
|
printf '%s\n' "frpc or webhook secret apply failed; manifest apply skipped" >"$tmp/apply.err"
|
|
fi
|
|
if [ "$apply_rc" -eq 0 ] && [ "$UNIDESK_GITEA_WAIT" = "1" ] && [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then
|
|
timeout "$UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS" kubectl -n "$UNIDESK_GITEA_NAMESPACE" rollout status "statefulset/$UNIDESK_GITEA_STATEFULSET_NAME" --timeout="${UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS}s" >"$tmp/rollout.out" 2>"$tmp/rollout.err"
|
|
rollout_rc=$?
|
|
else
|
|
rollout_rc=0
|
|
: >"$tmp/rollout.out"
|
|
if [ "$UNIDESK_GITEA_DRY_RUN" = "1" ]; then
|
|
printf '%s\n' "dry-run: rollout wait skipped" >"$tmp/rollout.err"
|
|
else
|
|
printf '%s\n' "rollout wait not requested" >"$tmp/rollout.err"
|
|
fi
|
|
fi
|
|
if [ "$apply_rc" -eq 0 ] && [ "$UNIDESK_GITEA_FRPC_ENABLED" = "1" ] && [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then
|
|
kubectl -n "$UNIDESK_GITEA_NAMESPACE" rollout restart "deployment/$UNIDESK_GITEA_FRPC_DEPLOYMENT_NAME" >"$tmp/frpc-rollout.out" 2>"$tmp/frpc-rollout.err"
|
|
frpc_rollout_rc=$?
|
|
if [ "$frpc_rollout_rc" -eq 0 ] && [ "$UNIDESK_GITEA_WAIT" = "1" ]; then
|
|
timeout "$UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS" kubectl -n "$UNIDESK_GITEA_NAMESPACE" rollout status "deployment/$UNIDESK_GITEA_FRPC_DEPLOYMENT_NAME" --timeout="${UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS}s" >>"$tmp/frpc-rollout.out" 2>>"$tmp/frpc-rollout.err"
|
|
frpc_rollout_rc=$?
|
|
fi
|
|
else
|
|
frpc_rollout_rc=0
|
|
: >"$tmp/frpc-rollout.out"
|
|
if [ "$UNIDESK_GITEA_DRY_RUN" = "1" ]; then
|
|
printf '%s\n' "dry-run: frpc rollout skipped" >"$tmp/frpc-rollout.err"
|
|
elif [ "$UNIDESK_GITEA_FRPC_ENABLED" = "1" ]; then
|
|
printf '%s\n' "manifest apply failed; frpc rollout skipped" >"$tmp/frpc-rollout.err"
|
|
else
|
|
printf '%s\n' "frpc disabled" >"$tmp/frpc-rollout.err"
|
|
fi
|
|
fi
|
|
if [ "$apply_rc" -eq 0 ] && [ "$UNIDESK_GITEA_WEBHOOK_SYNC_ENABLED" = "1" ] && [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then
|
|
kubectl -n "$UNIDESK_GITEA_NAMESPACE" rollout restart "deployment/$UNIDESK_GITEA_WEBHOOK_DEPLOYMENT" >"$tmp/webhook-rollout.out" 2>"$tmp/webhook-rollout.err"
|
|
webhook_rollout_rc=$?
|
|
if [ "$webhook_rollout_rc" -eq 0 ] && [ "$UNIDESK_GITEA_WAIT" = "1" ]; then
|
|
timeout "$UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS" kubectl -n "$UNIDESK_GITEA_NAMESPACE" rollout status "deployment/$UNIDESK_GITEA_WEBHOOK_DEPLOYMENT" --timeout="${UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS}s" >>"$tmp/webhook-rollout.out" 2>>"$tmp/webhook-rollout.err"
|
|
webhook_rollout_rc=$?
|
|
fi
|
|
else
|
|
webhook_rollout_rc=0
|
|
: >"$tmp/webhook-rollout.out"
|
|
if [ "$UNIDESK_GITEA_DRY_RUN" = "1" ]; then
|
|
printf '%s\n' "dry-run: webhook sync rollout skipped" >"$tmp/webhook-rollout.err"
|
|
elif [ "$UNIDESK_GITEA_WEBHOOK_SYNC_ENABLED" = "1" ]; then
|
|
printf '%s\n' "manifest apply failed; webhook sync rollout skipped" >"$tmp/webhook-rollout.err"
|
|
else
|
|
printf '%s\n' "webhook sync disabled" >"$tmp/webhook-rollout.err"
|
|
fi
|
|
fi
|
|
python3 - "$frpc_rc" "$webhook_secret_rc" "$apply_rc" "$rollout_rc" "$frpc_rollout_rc" "$webhook_rollout_rc" "$tmp/frpc-secret.out" "$tmp/frpc-secret.err" "$tmp/webhook-secret.out" "$tmp/webhook-secret.err" "$tmp/apply.out" "$tmp/apply.err" "$tmp/rollout.out" "$tmp/rollout.err" "$tmp/frpc-rollout.out" "$tmp/frpc-rollout.err" "$tmp/webhook-rollout.out" "$tmp/webhook-rollout.err" <<'PY'
|
|
import json, os, sys
|
|
frpc_rc, webhook_secret_rc, apply_rc, rollout_rc, frpc_rollout_rc, webhook_rollout_rc = [int(value) for value in sys.argv[1:7]]
|
|
def text(path, limit=3000):
|
|
try:
|
|
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
|
|
except FileNotFoundError:
|
|
return ""
|
|
dry = os.environ.get("UNIDESK_GITEA_DRY_RUN") == "1"
|
|
payload = {
|
|
"ok": all(value == 0 for value in [frpc_rc, webhook_secret_rc, apply_rc, rollout_rc, frpc_rollout_rc, webhook_rollout_rc]),
|
|
"target": os.environ.get("UNIDESK_GITEA_TARGET_ID"),
|
|
"route": os.environ.get("UNIDESK_GITEA_ROUTE"),
|
|
"namespace": os.environ.get("UNIDESK_GITEA_NAMESPACE"),
|
|
"mode": "dry-run" if dry else "confirmed",
|
|
"mutation": not dry,
|
|
"image": os.environ.get("UNIDESK_GITEA_IMAGE"),
|
|
"objects": {
|
|
"statefulSet": os.environ.get("UNIDESK_GITEA_STATEFULSET_NAME"),
|
|
"service": os.environ.get("UNIDESK_GITEA_SERVICE_NAME"),
|
|
"networkPolicy": "allow-all",
|
|
},
|
|
"steps": {
|
|
"frpcSecret": {"exitCode": frpc_rc, "stdoutTail": text(sys.argv[7]), "stderrTail": text(sys.argv[8])},
|
|
"webhookSyncSecret": {"exitCode": webhook_secret_rc, "stdoutTail": text(sys.argv[9]), "stderrTail": text(sys.argv[10])},
|
|
"apply": {"exitCode": apply_rc, "stdoutTail": text(sys.argv[11]), "stderrTail": text(sys.argv[12])},
|
|
"rollout": {"exitCode": rollout_rc, "stdoutTail": text(sys.argv[13]), "stderrTail": text(sys.argv[14])},
|
|
"frpcRollout": {"exitCode": frpc_rollout_rc, "stdoutTail": text(sys.argv[15]), "stderrTail": text(sys.argv[16])},
|
|
"webhookSyncRollout": {"exitCode": webhook_rollout_rc, "stdoutTail": text(sys.argv[17]), "stderrTail": text(sys.argv[18])},
|
|
},
|
|
"valuesPrinted": False,
|
|
}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
sys.exit(0 if payload["ok"] else 1)
|
|
PY
|
|
}
|
|
|
|
run_status() {
|
|
selector="app.kubernetes.io/name=$UNIDESK_GITEA_APP_NAME,app.kubernetes.io/component=gitea"
|
|
capture_json namespace kubectl get namespace "$UNIDESK_GITEA_NAMESPACE"
|
|
capture_json networkpolicy kubectl -n "$UNIDESK_GITEA_NAMESPACE" get networkpolicy allow-all
|
|
capture_json statefulset kubectl -n "$UNIDESK_GITEA_NAMESPACE" get statefulset "$UNIDESK_GITEA_STATEFULSET_NAME"
|
|
capture_json service kubectl -n "$UNIDESK_GITEA_NAMESPACE" get service "$UNIDESK_GITEA_SERVICE_NAME"
|
|
capture_json endpoints kubectl -n "$UNIDESK_GITEA_NAMESPACE" get endpoints "$UNIDESK_GITEA_SERVICE_NAME"
|
|
capture_json pods kubectl -n "$UNIDESK_GITEA_NAMESPACE" get pods -l "$selector"
|
|
capture_json pvc kubectl -n "$UNIDESK_GITEA_NAMESPACE" get pvc -l "$selector"
|
|
capture_json events kubectl -n "$UNIDESK_GITEA_NAMESPACE" get events --sort-by=.lastTimestamp
|
|
python3 - "$tmp" <<'PY'
|
|
import json, os, sys
|
|
tmp = sys.argv[1]
|
|
def rc(name):
|
|
try:
|
|
return int(open(f"{tmp}/{name}.rc", encoding="utf-8").read() or "1")
|
|
except FileNotFoundError:
|
|
return 1
|
|
def load(name):
|
|
try:
|
|
return json.load(open(f"{tmp}/{name}.json", encoding="utf-8"))
|
|
except Exception:
|
|
return None
|
|
def items(name):
|
|
data = load(name)
|
|
if isinstance(data, dict) and isinstance(data.get("items"), list):
|
|
return data["items"]
|
|
if isinstance(data, dict) and data.get("kind") != "Status":
|
|
return [data]
|
|
return []
|
|
def meta_name(item):
|
|
return ((item or {}).get("metadata") or {}).get("name")
|
|
def pod_ready(item):
|
|
status = (item or {}).get("status") or {}
|
|
return any(c.get("type") == "Ready" and c.get("status") == "True" for c in status.get("conditions", []))
|
|
def pod_summary(item):
|
|
status = (item or {}).get("status") or {}
|
|
containers = status.get("containerStatuses") or []
|
|
return {
|
|
"name": meta_name(item),
|
|
"phase": status.get("phase"),
|
|
"ready": pod_ready(item),
|
|
"restarts": sum(int(c.get("restartCount") or 0) for c in containers if isinstance(c, dict)),
|
|
}
|
|
def service_summary(item):
|
|
spec = (item or {}).get("spec") or {}
|
|
return {
|
|
"name": meta_name(item),
|
|
"type": spec.get("type"),
|
|
"clusterIP": spec.get("clusterIP"),
|
|
"ports": [{"name": p.get("name"), "port": p.get("port"), "targetPort": p.get("targetPort")} for p in spec.get("ports", [])],
|
|
}
|
|
def endpoint_ready(item):
|
|
subsets = ((item or {}).get("subsets") or [])
|
|
return any(s.get("addresses") for s in subsets if isinstance(s, dict))
|
|
def pvc_summary(item):
|
|
spec = (item or {}).get("spec") or {}
|
|
status = (item or {}).get("status") or {}
|
|
return {
|
|
"name": meta_name(item),
|
|
"phase": status.get("phase"),
|
|
"storageClassName": spec.get("storageClassName"),
|
|
"capacity": (status.get("capacity") or {}).get("storage"),
|
|
}
|
|
def event_tail(limit=8):
|
|
result = []
|
|
for item in items("events")[-limit:]:
|
|
result.append({
|
|
"type": item.get("type"),
|
|
"reason": item.get("reason"),
|
|
"object": f"{((item.get('involvedObject') or {}).get('kind') or '-')}/{((item.get('involvedObject') or {}).get('name') or '-')}",
|
|
"message": (item.get("message") or "")[:300],
|
|
"lastTimestamp": item.get("lastTimestamp") or item.get("eventTime"),
|
|
})
|
|
return result
|
|
sts = load("statefulset") or {}
|
|
sts_spec = sts.get("spec") or {}
|
|
sts_status = sts.get("status") or {}
|
|
desired = int(sts_spec.get("replicas") or 0)
|
|
ready_replicas = int(sts_status.get("readyReplicas") or 0)
|
|
pods = [pod_summary(item) for item in items("pods")]
|
|
service = service_summary(load("service") or {})
|
|
endpoint_ok = endpoint_ready(load("endpoints") or {})
|
|
ready = rc("namespace") == 0 and rc("networkpolicy") == 0 and rc("service") == 0 and desired > 0 and ready_replicas >= desired and endpoint_ok
|
|
payload = {
|
|
"ok": True,
|
|
"ready": ready,
|
|
"target": os.environ.get("UNIDESK_GITEA_TARGET_ID"),
|
|
"route": os.environ.get("UNIDESK_GITEA_ROUTE"),
|
|
"namespace": os.environ.get("UNIDESK_GITEA_NAMESPACE"),
|
|
"image": os.environ.get("UNIDESK_GITEA_IMAGE"),
|
|
"serviceDns": f"{os.environ.get('UNIDESK_GITEA_SERVICE_NAME')}.{os.environ.get('UNIDESK_GITEA_NAMESPACE')}.svc.cluster.local:{os.environ.get('UNIDESK_GITEA_HTTP_PORT')}",
|
|
"networkPolicy": {"allowAllPresent": rc("networkpolicy") == 0},
|
|
"statefulSet": {"name": meta_name(sts), "desired": desired, "readyReplicas": ready_replicas, "updatedReplicas": sts_status.get("updatedReplicas")},
|
|
"service": service,
|
|
"endpointsReady": endpoint_ok,
|
|
"pods": pods,
|
|
"pvcs": [pvc_summary(item) for item in items("pvc")],
|
|
"eventsTail": event_tail(),
|
|
"valuesPrinted": False,
|
|
}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
sys.exit(0)
|
|
PY
|
|
}
|
|
|
|
run_validate() {
|
|
proxy_path="/api/v1/namespaces/$UNIDESK_GITEA_NAMESPACE/services/http:$UNIDESK_GITEA_SERVICE_NAME:$UNIDESK_GITEA_HTTP_PORT/proxy$UNIDESK_GITEA_HEALTH_PATH"
|
|
kubectl get --raw "$proxy_path" >"$tmp/health.out" 2>"$tmp/health.err"
|
|
health_rc=$?
|
|
capture_json endpoints kubectl -n "$UNIDESK_GITEA_NAMESPACE" get endpoints "$UNIDESK_GITEA_SERVICE_NAME"
|
|
python3 - "$health_rc" "$tmp/health.out" "$tmp/health.err" "$tmp/endpoints.json" "$tmp/endpoints.rc" <<'PY'
|
|
import json, os, sys
|
|
health_rc = int(sys.argv[1])
|
|
def text(path, limit=2000):
|
|
try:
|
|
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
|
|
except FileNotFoundError:
|
|
return ""
|
|
body = text(sys.argv[2], 4000)
|
|
try:
|
|
parsed = json.loads(body)
|
|
except Exception:
|
|
parsed = None
|
|
try:
|
|
endpoint_rc = int(open(sys.argv[5], encoding="utf-8").read() or "1")
|
|
except Exception:
|
|
endpoint_rc = 1
|
|
try:
|
|
endpoints = json.load(open(sys.argv[4], encoding="utf-8"))
|
|
except Exception:
|
|
endpoints = {}
|
|
subsets = endpoints.get("subsets") if isinstance(endpoints, dict) else []
|
|
endpoints_ready = any(isinstance(s, dict) and s.get("addresses") for s in (subsets or []))
|
|
health_status = parsed.get("status") if isinstance(parsed, dict) else None
|
|
ok = health_rc == 0 and endpoint_rc == 0 and endpoints_ready and health_status == "pass"
|
|
payload = {
|
|
"ok": ok,
|
|
"target": os.environ.get("UNIDESK_GITEA_TARGET_ID"),
|
|
"route": os.environ.get("UNIDESK_GITEA_ROUTE"),
|
|
"namespace": os.environ.get("UNIDESK_GITEA_NAMESPACE"),
|
|
"service": os.environ.get("UNIDESK_GITEA_SERVICE_NAME"),
|
|
"serviceProxyPath": f"/api/v1/namespaces/{os.environ.get('UNIDESK_GITEA_NAMESPACE')}/services/http:{os.environ.get('UNIDESK_GITEA_SERVICE_NAME')}:{os.environ.get('UNIDESK_GITEA_HTTP_PORT')}/proxy{os.environ.get('UNIDESK_GITEA_HEALTH_PATH')}",
|
|
"health": {
|
|
"exitCode": health_rc,
|
|
"status": health_status,
|
|
"description": parsed.get("description") if isinstance(parsed, dict) else None,
|
|
"bodyBytes": len(body.encode("utf-8")),
|
|
"stderrTail": text(sys.argv[3]),
|
|
},
|
|
"endpointsReady": endpoints_ready,
|
|
"valuesPrinted": False,
|
|
}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
sys.exit(0 if ok else 1)
|
|
PY
|
|
}
|
|
|
|
service_base_url() {
|
|
kubectl -n "$UNIDESK_GITEA_NAMESPACE" get svc "$UNIDESK_GITEA_SERVICE_NAME" -o json >"$tmp/gitea-svc.json" 2>"$tmp/gitea-svc.err"
|
|
python3 - "$tmp/gitea-svc.json" "$UNIDESK_GITEA_HTTP_PORT" <<'PY'
|
|
import json, sys
|
|
data = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
print(f"http://{data['spec']['clusterIP']}:{sys.argv[2]}")
|
|
PY
|
|
}
|
|
|
|
mirror_repos_json() {
|
|
printf '%s' "$UNIDESK_GITEA_REPOS_B64" | base64 -d >"$tmp/repos.json"
|
|
}
|
|
|
|
admin_basic_b64() {
|
|
python3 - <<'PY'
|
|
import base64, os
|
|
login = os.environ["UNIDESK_GITEA_ADMIN_USERNAME"]
|
|
username = login.split("@", 1)[0] if "@" in login else login
|
|
raw = f"{username}:{os.environ['UNIDESK_GITEA_ADMIN_PASSWORD']}".encode()
|
|
print(base64.b64encode(raw).decode())
|
|
PY
|
|
}
|
|
|
|
admin_cli_username() {
|
|
python3 - <<'PY'
|
|
import os
|
|
login = os.environ["UNIDESK_GITEA_ADMIN_USERNAME"]
|
|
print(login.split("@", 1)[0] if "@" in login else login)
|
|
PY
|
|
}
|
|
|
|
run_mirror_bootstrap() {
|
|
mirror_repos_json
|
|
pod="$(kubectl -n "$UNIDESK_GITEA_NAMESPACE" get pod -l "app.kubernetes.io/name=$UNIDESK_GITEA_APP_NAME,app.kubernetes.io/component=gitea" -o jsonpath='{.items[0].metadata.name}' 2>"$tmp/pod.err")"
|
|
create_rc=1
|
|
change_rc=1
|
|
unset_rc=1
|
|
if [ -n "$pod" ]; then
|
|
admin_username="$(admin_cli_username)"
|
|
admin_email="$UNIDESK_GITEA_ADMIN_USERNAME"
|
|
case "$admin_email" in
|
|
*@*) ;;
|
|
*) admin_email="$admin_username@local" ;;
|
|
esac
|
|
kubectl -n "$UNIDESK_GITEA_NAMESPACE" exec "$pod" -- gitea --work-path /var/lib/gitea --config /etc/gitea/app.ini admin user create --username "$admin_username" --password "$UNIDESK_GITEA_ADMIN_PASSWORD" --email "$admin_email" --admin --must-change-password=false >"$tmp/admin-create.out" 2>"$tmp/admin-create.err"
|
|
create_rc=$?
|
|
if [ "$create_rc" -ne 0 ] && grep -qi 'user already exists' "$tmp/admin-create.err"; then
|
|
create_rc=0
|
|
fi
|
|
kubectl -n "$UNIDESK_GITEA_NAMESPACE" exec "$pod" -- gitea --work-path /var/lib/gitea --config /etc/gitea/app.ini admin user change-password --username "$admin_username" --password "$UNIDESK_GITEA_ADMIN_PASSWORD" --must-change-password=false >"$tmp/admin-pass.out" 2>"$tmp/admin-pass.err"
|
|
change_rc=$?
|
|
kubectl -n "$UNIDESK_GITEA_NAMESPACE" exec "$pod" -- gitea --work-path /var/lib/gitea --config /etc/gitea/app.ini admin user must-change-password --unset "$admin_username" >"$tmp/admin-unset.out" 2>"$tmp/admin-unset.err"
|
|
unset_rc=$?
|
|
else
|
|
printf '%s\n' "gitea pod not found" >"$tmp/admin-create.err"
|
|
: >"$tmp/admin-create.out"
|
|
: >"$tmp/admin-pass.out"
|
|
: >"$tmp/admin-pass.err"
|
|
: >"$tmp/admin-unset.out"
|
|
: >"$tmp/admin-unset.err"
|
|
fi
|
|
|
|
base_url="$(service_base_url)"
|
|
basic="$(admin_basic_b64)"
|
|
python3 - "$base_url" "$basic" "$tmp/repos.json" "$tmp/api.json" <<'PY'
|
|
import json, sys, traceback, urllib.error, urllib.request
|
|
base_url, basic, repos_path, out_path = sys.argv[1:5]
|
|
def request(method, path, payload=None, expected=(200, 201, 204), tolerate=()):
|
|
data = None if payload is None else json.dumps(payload).encode()
|
|
req = urllib.request.Request(base_url + path, data=data, method=method)
|
|
req.add_header("Authorization", "Basic " + basic)
|
|
if payload is not None:
|
|
req.add_header("Content-Type", "application/json")
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=20) as resp:
|
|
body = resp.read().decode("utf-8", errors="replace")
|
|
return {"ok": resp.status in expected, "status": resp.status, "body": body[:1000]}
|
|
except urllib.error.HTTPError as exc:
|
|
body = exc.read().decode("utf-8", errors="replace")
|
|
return {"ok": exc.code in tolerate, "status": exc.code, "body": body[:1000]}
|
|
try:
|
|
repos = json.load(open(repos_path, encoding="utf-8"))
|
|
orgs = {}
|
|
repositories = []
|
|
auth = request("GET", "/api/v1/user")
|
|
for repo in repos:
|
|
owner = repo["gitea"]["owner"]
|
|
if owner not in orgs:
|
|
orgs[owner] = {
|
|
"create": request("POST", "/api/v1/orgs", {"username": owner, "full_name": "UniDesk internal mirrors", "visibility": "public"}, expected=(201,), tolerate=(409, 422)),
|
|
"patch": request("PATCH", f"/api/v1/orgs/{owner}", {"full_name": "UniDesk internal mirrors", "visibility": "public"}, expected=(200,), tolerate=()),
|
|
}
|
|
repositories.append({
|
|
"key": repo["key"],
|
|
"owner": owner,
|
|
"name": repo["gitea"]["name"],
|
|
"create": request("POST", f"/api/v1/orgs/{owner}/repos", {"name": repo["gitea"]["name"], "private": not bool(repo["gitea"].get("publicRead")), "auto_init": False, "description": f"UniDesk controlled mirror for {repo['upstream']['repository']}"}, expected=(201,), tolerate=(409, 422)),
|
|
"patch": request("PATCH", f"/api/v1/repos/{owner}/{repo['gitea']['name']}", {"private": not bool(repo["gitea"].get("publicRead")), "description": f"UniDesk controlled mirror for {repo['upstream']['repository']}"}, expected=(200,), tolerate=()),
|
|
})
|
|
payload = {"auth": auth, "orgs": orgs, "repositories": repositories}
|
|
ok = auth.get("ok") and all(v["create"].get("ok") and v["patch"].get("ok") for v in orgs.values()) and all(item["create"].get("ok") and item["patch"].get("ok") for item in repositories)
|
|
except Exception as exc:
|
|
payload = {
|
|
"auth": {"ok": False},
|
|
"orgs": {},
|
|
"repositories": [],
|
|
"error": str(exc),
|
|
"traceTail": traceback.format_exc()[-1600:],
|
|
}
|
|
ok = False
|
|
json.dump(payload, open(out_path, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
|
|
sys.exit(0 if ok else 1)
|
|
PY
|
|
api_rc=$?
|
|
python3 - "$create_rc" "$change_rc" "$unset_rc" "$api_rc" "$tmp/admin-create.out" "$tmp/admin-create.err" "$tmp/admin-pass.out" "$tmp/admin-pass.err" "$tmp/admin-unset.out" "$tmp/admin-unset.err" "$tmp/api.json" <<'PY'
|
|
import json, sys
|
|
def text(path, limit=1600):
|
|
try:
|
|
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
|
|
except FileNotFoundError:
|
|
return ""
|
|
create_rc, change_rc, unset_rc, api_rc = map(int, sys.argv[1:5])
|
|
try:
|
|
api = json.load(open(sys.argv[11], encoding="utf-8"))
|
|
except Exception:
|
|
api = {}
|
|
payload = {
|
|
"ok": create_rc == 0 and change_rc == 0 and unset_rc == 0 and api_rc == 0,
|
|
"target": __import__("os").environ.get("UNIDESK_GITEA_TARGET_ID"),
|
|
"namespace": __import__("os").environ.get("UNIDESK_GITEA_NAMESPACE"),
|
|
"steps": {
|
|
"adminCreate": {"exitCode": create_rc, "stdoutTail": text(sys.argv[5]), "stderrTail": text(sys.argv[6])},
|
|
"adminPassword": {"exitCode": change_rc, "stdoutTail": text(sys.argv[7]), "stderrTail": text(sys.argv[8])},
|
|
"adminMustChangePasswordUnset": {"exitCode": unset_rc, "stdoutTail": text(sys.argv[9]), "stderrTail": text(sys.argv[10])},
|
|
"apiBootstrap": {"exitCode": api_rc},
|
|
},
|
|
"api": api,
|
|
"repositories": api.get("repositories", []),
|
|
"valuesPrinted": False,
|
|
}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
sys.exit(0 if payload["ok"] else 1)
|
|
PY
|
|
}
|
|
|
|
run_mirror_sync() {
|
|
mirror_repos_json
|
|
base_url="$(service_base_url)"
|
|
basic="$(admin_basic_b64)"
|
|
python3 - "$tmp/repos.json" "$tmp/sync-plan.sh" "$tmp/repo-meta.json" "$base_url" <<'PY'
|
|
import json, shlex, sys, urllib.parse
|
|
repos = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
base_url = sys.argv[4].rstrip("/")
|
|
script = []
|
|
meta = []
|
|
for i, repo in enumerate(repos):
|
|
key = repo["key"]
|
|
source_branch = repo["upstream"]["branch"]
|
|
gitops_branch = repo["gitops"]["branch"]
|
|
branches = [source_branch] + ([] if gitops_branch == source_branch or gitops_branch == "not-a-gitops-branch" else [gitops_branch])
|
|
work = f"$tmp/repo-{i}.git"
|
|
gitea_write_url = base_url + urllib.parse.urlparse(repo["gitea"]["readUrl"]).path
|
|
script += [
|
|
f"mkdir -p {shlex.quote(work)}",
|
|
f"git init --bare {shlex.quote(work)} >$tmp/{key}.init.out 2>$tmp/{key}.init.err; printf '%s' \"$?\" >$tmp/{key}.init.rc",
|
|
f"git -C {shlex.quote(work)} -c http.extraHeader=\"$GITHUB_AUTH_HEADER\" fetch --prune {shlex.quote(repo['upstream']['cloneUrl'])} " + " ".join(shlex.quote(f"+refs/heads/{b}:refs/remotes/github/{b}") for b in branches) + f" >$tmp/{key}.fetch.out 2>$tmp/{key}.fetch.err; fetch_rc=$?; printf '%s' \"$fetch_rc\" >$tmp/{key}.fetch.rc",
|
|
f"if [ \"$fetch_rc\" -eq 0 ]; then sha=$(git -C {shlex.quote(work)} rev-parse refs/remotes/github/{shlex.quote(source_branch)} 2>$tmp/{key}.revparse.err); revparse_rc=$?; printf '%s\\n' \"$sha\" >$tmp/{key}.revparse.out; else sha=''; revparse_rc=1; : >$tmp/{key}.revparse.out; printf '%s\\n' 'fetch failed; revparse skipped' >$tmp/{key}.revparse.err; fi; printf '%s' \"$revparse_rc\" >$tmp/{key}.revparse.rc",
|
|
f"if [ \"$revparse_rc\" -eq 0 ]; then snapshot_ref={shlex.quote(repo['snapshot']['prefix'])}/$sha; git -C {shlex.quote(work)} -c http.extraHeader=\"$GITEA_AUTH_HEADER\" push --force {shlex.quote(gitea_write_url)} $sha:refs/heads/{shlex.quote(source_branch)} $sha:$snapshot_ref >$tmp/{key}.push.out 2>$tmp/{key}.push.err; push_rc=$?; else snapshot_ref=''; push_rc=1; : >$tmp/{key}.push.out; printf '%s\\n' 'revparse failed; push skipped' >$tmp/{key}.push.err; fi; printf '%s' \"$push_rc\" >$tmp/{key}.push.rc",
|
|
]
|
|
if gitops_branch != source_branch and gitops_branch != "not-a-gitops-branch":
|
|
script.append(f"if [ \"$fetch_rc\" -eq 0 ]; then gitops_sha=$(git -C {shlex.quote(work)} rev-parse refs/remotes/github/{shlex.quote(gitops_branch)} 2>/dev/null || true); else gitops_sha=''; fi")
|
|
script.append(f"if [ -n \"$gitops_sha\" ] && [ \"$push_rc\" -eq 0 ]; then git -C {shlex.quote(work)} -c http.extraHeader=\"$GITEA_AUTH_HEADER\" push --force {shlex.quote(gitea_write_url)} $gitops_sha:refs/heads/{shlex.quote(gitops_branch)} >>$tmp/{key}.push.out 2>>$tmp/{key}.push.err; gitops_push_rc=$?; else gitops_push_rc=0; fi; printf '%s' \"$gitops_push_rc\" >$tmp/{key}.gitops-push.rc")
|
|
else:
|
|
script.append(f"printf '%s' '0' >$tmp/{key}.gitops-push.rc")
|
|
script.append(f"if [ \"$push_rc\" -eq 0 ]; then printf '%s %s\\n' \"$sha\" \"$snapshot_ref\" >$tmp/{key}.bundle; fi")
|
|
meta.append({"key": key, "sourceBranch": source_branch, "gitopsBranch": gitops_branch, "readUrl": repo["gitea"]["readUrl"], "writeUrlHost": urllib.parse.urlparse(gitea_write_url).netloc, "snapshotPrefix": repo["snapshot"]["prefix"], "legacyReadUrl": repo["legacyGitMirror"]["readUrl"]})
|
|
open(sys.argv[2], "w", encoding="utf-8").write("\n".join(script) + "\n")
|
|
json.dump(meta, open(sys.argv[3], "w", encoding="utf-8"), ensure_ascii=False, indent=2)
|
|
PY
|
|
github_basic="$(python3 - <<'PY'
|
|
import base64, os
|
|
raw = f"x-access-token:{os.environ['UNIDESK_GITEA_GITHUB_TOKEN']}".encode()
|
|
print(base64.b64encode(raw).decode())
|
|
PY
|
|
)"
|
|
GITHUB_AUTH_HEADER="Authorization: Basic $github_basic"
|
|
GITEA_AUTH_HEADER="Authorization: Basic $basic"
|
|
export GITHUB_AUTH_HEADER GITEA_AUTH_HEADER
|
|
unset GIT_SSH GIT_SSH_COMMAND SSH_AUTH_SOCK
|
|
export GIT_TERMINAL_PROMPT=0
|
|
export GIT_CONFIG_NOSYSTEM=1
|
|
export GIT_CONFIG_GLOBAL=/dev/null
|
|
if [ "$UNIDESK_GITEA_GITHUB_PROXY_ENABLED" = "1" ]; then
|
|
export https_proxy="$UNIDESK_GITEA_GITHUB_PROXY_URL"
|
|
export http_proxy="$UNIDESK_GITEA_GITHUB_PROXY_URL"
|
|
export no_proxy="$UNIDESK_GITEA_NO_PROXY"
|
|
fi
|
|
sh "$tmp/sync-plan.sh" >"$tmp/sync.out" 2>"$tmp/sync.err"
|
|
sync_rc=$?
|
|
python3 - "$sync_rc" "$tmp/repo-meta.json" "$tmp" <<'PY'
|
|
import json, os, sys
|
|
sync_rc = int(sys.argv[1])
|
|
meta = json.load(open(sys.argv[2], encoding="utf-8"))
|
|
tmp = sys.argv[3]
|
|
def text(path, limit=1200):
|
|
try:
|
|
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
|
|
except FileNotFoundError:
|
|
return ""
|
|
def rc(path):
|
|
try:
|
|
return int(open(path, encoding="utf-8").read() or "1")
|
|
except Exception:
|
|
return 1
|
|
repos = []
|
|
bundles = []
|
|
for repo in meta:
|
|
bundle_path = os.path.join(tmp, f"{repo['key']}.bundle")
|
|
source_commit = None
|
|
snapshot_ref = None
|
|
try:
|
|
parts = open(bundle_path, encoding="utf-8").read().strip().split()
|
|
source_commit, snapshot_ref = parts[0], parts[1]
|
|
except Exception:
|
|
pass
|
|
repos.append({
|
|
**repo,
|
|
"sourceCommit": source_commit,
|
|
"snapshotRef": snapshot_ref,
|
|
"syncOk": source_commit is not None and snapshot_ref is not None and rc(os.path.join(tmp, f"{repo['key']}.gitops-push.rc")) == 0,
|
|
"initRc": rc(os.path.join(tmp, f"{repo['key']}.init.rc")),
|
|
"fetchRc": rc(os.path.join(tmp, f"{repo['key']}.fetch.rc")),
|
|
"revparseRc": rc(os.path.join(tmp, f"{repo['key']}.revparse.rc")),
|
|
"pushRc": rc(os.path.join(tmp, f"{repo['key']}.push.rc")),
|
|
"gitopsPushRc": rc(os.path.join(tmp, f"{repo['key']}.gitops-push.rc")),
|
|
"fetchStdoutTail": text(os.path.join(tmp, f"{repo['key']}.fetch.out")),
|
|
"fetchTail": text(os.path.join(tmp, f"{repo['key']}.fetch.err")),
|
|
"pushStdoutTail": text(os.path.join(tmp, f"{repo['key']}.push.out")),
|
|
"pushTail": text(os.path.join(tmp, f"{repo['key']}.push.err")),
|
|
"revparseTail": text(os.path.join(tmp, f"{repo['key']}.revparse.err")),
|
|
})
|
|
if source_commit and snapshot_ref:
|
|
bundles.append({"key": repo["key"], "sourceCommit": source_commit, "snapshotRef": snapshot_ref, "readUrl": repo["readUrl"], "sourceAuthority": "gitea"})
|
|
payload = {"ok": sync_rc == 0 and all(r["syncOk"] for r in repos), "repositories": repos, "sourceBundles": bundles, "valuesPrinted": False}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
sys.exit(0 if payload["ok"] else 1)
|
|
PY
|
|
}
|
|
|
|
run_mirror_status() {
|
|
mirror_repos_json
|
|
base_url="$(service_base_url)"
|
|
basic="$(admin_basic_b64)"
|
|
python3 - "$tmp/repos.json" "$tmp/status-plan.sh" "$tmp/repo-meta.json" "$base_url" <<'PY'
|
|
import json, shlex, sys, urllib.parse
|
|
repos = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
base_url = sys.argv[4].rstrip("/")
|
|
script = []
|
|
meta = []
|
|
for repo in repos:
|
|
key = repo["key"]
|
|
branch = repo["upstream"]["branch"]
|
|
gitops = repo["gitops"]["branch"]
|
|
gitea_probe_url = base_url + urllib.parse.urlparse(repo["gitea"]["readUrl"]).path
|
|
script.append(f"git -c http.extraHeader=\"$GITEA_AUTH_HEADER\" ls-remote {shlex.quote(gitea_probe_url)} refs/heads/{shlex.quote(branch)} '{shlex.quote(repo['snapshot']['prefix'])}/*' refs/heads/{shlex.quote(gitops)} >$tmp/{key}.ls.out 2>$tmp/{key}.ls.err")
|
|
meta.append({"key": key, "branch": branch, "gitopsBranch": gitops, "readUrl": repo["gitea"]["readUrl"], "probeUrlHost": urllib.parse.urlparse(gitea_probe_url).netloc, "snapshotPrefix": repo["snapshot"]["prefix"], "legacyReadUrl": repo["legacyGitMirror"]["readUrl"], "legacyDisposition": repo["legacyGitMirror"]["disposition"]})
|
|
open(sys.argv[2], "w", encoding="utf-8").write("\n".join(script) + "\n")
|
|
json.dump(meta, open(sys.argv[3], "w", encoding="utf-8"), ensure_ascii=False)
|
|
PY
|
|
GITEA_AUTH_HEADER="Authorization: Basic $basic"
|
|
export GITEA_AUTH_HEADER
|
|
sh "$tmp/status-plan.sh" >"$tmp/status.out" 2>"$tmp/status.err"
|
|
status_rc=$?
|
|
python3 - "$status_rc" "$tmp/repo-meta.json" "$tmp" <<'PY'
|
|
import json, os, sys
|
|
status_rc = int(sys.argv[1])
|
|
meta = json.load(open(sys.argv[2], encoding="utf-8"))
|
|
tmp = sys.argv[3]
|
|
def text(path, limit=1200):
|
|
try:
|
|
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
|
|
except FileNotFoundError:
|
|
return ""
|
|
repos = []
|
|
for repo in meta:
|
|
out = text(os.path.join(tmp, f"{repo['key']}.ls.out"), 20000)
|
|
refs = {}
|
|
for line in out.splitlines():
|
|
parts = line.split()
|
|
if len(parts) == 2:
|
|
refs[parts[1]] = parts[0]
|
|
branch_ref = f"refs/heads/{repo['branch']}"
|
|
branch_commit = refs.get(branch_ref)
|
|
snapshots = sorted((ref, sha) for ref, sha in refs.items() if ref.startswith(repo["snapshotPrefix"] + "/"))
|
|
matching_snapshots = [item for item in snapshots if branch_commit and item[1] == branch_commit]
|
|
latest_snapshot = matching_snapshots[-1] if matching_snapshots else (snapshots[-1] if snapshots else None)
|
|
current_snapshot_ready = bool(branch_commit and latest_snapshot and latest_snapshot[1] == branch_commit)
|
|
repos.append({
|
|
**repo,
|
|
"branchCommit": branch_commit,
|
|
"snapshotRef": latest_snapshot[0] if latest_snapshot else None,
|
|
"snapshotCommit": latest_snapshot[1] if latest_snapshot else None,
|
|
"sourceBundleReady": current_snapshot_ready,
|
|
"mirrorState": "ready" if current_snapshot_ready else "missing-ref",
|
|
"errorTail": text(os.path.join(tmp, f"{repo['key']}.ls.err")),
|
|
})
|
|
payload = {"ok": status_rc == 0 and all(r["sourceBundleReady"] for r in repos), "repositories": repos, "valuesPrinted": False}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
sys.exit(0 if payload["ok"] else 1)
|
|
PY
|
|
}
|
|
|
|
github_api_env() {
|
|
if [ "$UNIDESK_GITEA_GITHUB_PROXY_ENABLED" = "1" ]; then
|
|
export https_proxy="$UNIDESK_GITEA_GITHUB_PROXY_URL"
|
|
export http_proxy="$UNIDESK_GITEA_GITHUB_PROXY_URL"
|
|
export no_proxy="$UNIDESK_GITEA_NO_PROXY"
|
|
fi
|
|
}
|
|
|
|
run_mirror_webhook_apply() {
|
|
mirror_repos_json
|
|
github_api_env
|
|
python3 - "$tmp/repos.json" <<'PY'
|
|
import json, os, sys, urllib.error, urllib.request
|
|
repos = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
token = os.environ["UNIDESK_GITEA_GITHUB_TOKEN"]
|
|
url = os.environ["UNIDESK_GITEA_WEBHOOK_PUBLIC_URL"]
|
|
secret = os.environ["UNIDESK_GITEA_GITHUB_WEBHOOK_SECRET"]
|
|
events = [item for item in os.environ.get("UNIDESK_GITEA_WEBHOOK_EVENTS", "push").split(",") if item]
|
|
def request(method, api_path, payload=None, expected=(200, 201, 204), tolerate=()):
|
|
data = None if payload is None else json.dumps(payload).encode()
|
|
req = urllib.request.Request("https://api.github.com" + api_path, data=data, method=method)
|
|
req.add_header("Authorization", "Bearer " + token)
|
|
req.add_header("Accept", "application/vnd.github+json")
|
|
req.add_header("X-GitHub-Api-Version", "2022-11-28")
|
|
if payload is not None:
|
|
req.add_header("Content-Type", "application/json")
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
body = resp.read().decode("utf-8", errors="replace")
|
|
return {"ok": resp.status in expected, "status": resp.status, "body": body}
|
|
except urllib.error.HTTPError as exc:
|
|
body = exc.read().decode("utf-8", errors="replace")
|
|
return {"ok": exc.code in tolerate, "status": exc.code, "body": body[:1000]}
|
|
rows = []
|
|
for repo in repos:
|
|
repository = repo["upstream"]["repository"]
|
|
list_result = request("GET", f"/repos/{repository}/hooks")
|
|
hooks = []
|
|
try:
|
|
hooks = json.loads(list_result.get("body") or "[]")
|
|
except Exception:
|
|
hooks = []
|
|
matches = [item for item in hooks if (item.get("config") or {}).get("url") == url]
|
|
body = {"name": "web", "active": True, "events": events, "config": {"url": url, "content_type": "json", "secret": secret, "insecure_ssl": "0"}}
|
|
if matches:
|
|
hook_id = matches[0].get("id")
|
|
result = request("PATCH", f"/repos/{repository}/hooks/{hook_id}", body)
|
|
for duplicate in matches[1:]:
|
|
duplicate_id = duplicate.get("id")
|
|
if duplicate_id:
|
|
request("DELETE", f"/repos/{repository}/hooks/{duplicate_id}", expected=(204,), tolerate=(404,))
|
|
else:
|
|
result = request("POST", f"/repos/{repository}/hooks", body, expected=(201,))
|
|
try:
|
|
hook_id = json.loads(result.get("body") or "{}").get("id")
|
|
except Exception:
|
|
hook_id = None
|
|
rows.append({"key": repo["key"], "repository": repository, "hookId": hook_id, "hookReady": result.get("ok") is True, "status": result.get("status"), "error": None if result.get("ok") else result.get("body", "")[:500], "valuesPrinted": False})
|
|
payload = {"ok": all(row["hookReady"] for row in rows), "repositories": rows, "url": url, "valuesPrinted": False}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
sys.exit(0 if payload["ok"] else 1)
|
|
PY
|
|
}
|
|
|
|
run_mirror_webhook_status() {
|
|
mirror_repos_json
|
|
github_api_env
|
|
bridge_ready=$(kubectl -n "$UNIDESK_GITEA_NAMESPACE" get deploy "$UNIDESK_GITEA_WEBHOOK_DEPLOYMENT" -o jsonpath='{.status.readyReplicas}/{.spec.replicas}' 2>/dev/null || echo "0/0")
|
|
service_exists=$(kubectl -n "$UNIDESK_GITEA_NAMESPACE" get service "$UNIDESK_GITEA_WEBHOOK_SERVICE" -o name 2>/dev/null || true)
|
|
python3 - "$tmp/repos.json" "$bridge_ready" "$service_exists" <<'PY'
|
|
import json, os, sys, urllib.error, urllib.request
|
|
repos = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
bridge_ready = sys.argv[2]
|
|
service_exists = bool(sys.argv[3])
|
|
token = os.environ["UNIDESK_GITEA_GITHUB_TOKEN"]
|
|
url = os.environ["UNIDESK_GITEA_WEBHOOK_PUBLIC_URL"]
|
|
def request(api_path):
|
|
req = urllib.request.Request("https://api.github.com" + api_path, method="GET")
|
|
req.add_header("Authorization", "Bearer " + token)
|
|
req.add_header("Accept", "application/vnd.github+json")
|
|
req.add_header("X-GitHub-Api-Version", "2022-11-28")
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
return {"ok": True, "status": resp.status, "body": resp.read().decode("utf-8", errors="replace")}
|
|
except urllib.error.HTTPError as exc:
|
|
return {"ok": False, "status": exc.code, "body": exc.read().decode("utf-8", errors="replace")[:1000]}
|
|
rows = []
|
|
for repo in repos:
|
|
repository = repo["upstream"]["repository"]
|
|
result = request(f"/repos/{repository}/hooks")
|
|
hooks = []
|
|
try:
|
|
hooks = json.loads(result.get("body") or "[]")
|
|
except Exception:
|
|
hooks = []
|
|
match = next((item for item in hooks if (item.get("config") or {}).get("url") == url), None)
|
|
rows.append({"key": repo["key"], "repository": repository, "hookId": match.get("id") if match else None, "hookReady": bool(match and match.get("active") is True), "active": match.get("active") if match else None, "status": result.get("status"), "error": None if result.get("ok") else result.get("body", "")[:500], "valuesPrinted": False})
|
|
bridge = {"deployment": os.environ["UNIDESK_GITEA_WEBHOOK_DEPLOYMENT"], "ready": bridge_ready not in ("0/0", "/") and bridge_ready.split("/")[0] == bridge_ready.split("/")[-1], "readyReplicas": bridge_ready, "serviceExists": service_exists}
|
|
payload = {"ok": bridge["ready"] and service_exists and all(row["hookReady"] for row in rows), "bridge": bridge, "repositories": rows, "url": url, "valuesPrinted": False}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
sys.exit(0 if payload["ok"] else 1)
|
|
PY
|
|
}
|
|
|
|
run_mirror_webhook_test() {
|
|
mirror_repos_json
|
|
repo_count=$(python3 - "$tmp/repos.json" <<'PY'
|
|
import json, sys
|
|
print(len(json.load(open(sys.argv[1], encoding="utf-8"))))
|
|
PY
|
|
)
|
|
if [ "$repo_count" != "1" ]; then
|
|
printf '{"ok":false,"error":"webhook test requires exactly one repo","valuesPrinted":false}\n'
|
|
exit 2
|
|
fi
|
|
repo_key=$(python3 - "$tmp/repos.json" <<'PY'
|
|
import json, sys
|
|
print(json.load(open(sys.argv[1], encoding="utf-8"))[0]["key"])
|
|
PY
|
|
)
|
|
github_api_env
|
|
python3 - "$tmp/repos.json" >"$tmp/webhook-ping.json" 2>"$tmp/webhook-ping.err" <<'PY'
|
|
import json, os, sys, time, urllib.error, urllib.request
|
|
repo = json.load(open(sys.argv[1], encoding="utf-8"))[0]
|
|
repository = repo["upstream"]["repository"]
|
|
token = os.environ["UNIDESK_GITEA_GITHUB_TOKEN"]
|
|
url = os.environ["UNIDESK_GITEA_WEBHOOK_PUBLIC_URL"]
|
|
def request(method, path, payload=None, expected=(200, 201, 204)):
|
|
data = None if payload is None else json.dumps(payload).encode()
|
|
req = urllib.request.Request("https://api.github.com" + path, data=data, method=method)
|
|
req.add_header("Authorization", "Bearer " + token)
|
|
req.add_header("Accept", "application/vnd.github+json")
|
|
req.add_header("X-GitHub-Api-Version", "2022-11-28")
|
|
if payload is not None:
|
|
req.add_header("Content-Type", "application/json")
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
return {"ok": resp.status in expected, "status": resp.status, "body": resp.read().decode("utf-8", errors="replace")}
|
|
except urllib.error.HTTPError as exc:
|
|
return {"ok": False, "status": exc.code, "body": exc.read().decode("utf-8", errors="replace")[:1000]}
|
|
hooks_result = request("GET", f"/repos/{repository}/hooks")
|
|
hooks = json.loads(hooks_result.get("body") or "[]") if hooks_result.get("ok") else []
|
|
hook = next((item for item in hooks if (item.get("config") or {}).get("url") == url), None)
|
|
hook_id = hook.get("id") if hook else None
|
|
ping = request("POST", f"/repos/{repository}/hooks/{hook_id}/pings", expected=(204,)) if hook_id else {"ok": False, "status": None, "body": "hook not found"}
|
|
delivery = None
|
|
if hook_id and ping.get("ok"):
|
|
time.sleep(3)
|
|
deliveries = request("GET", f"/repos/{repository}/hooks/{hook_id}/deliveries")
|
|
try:
|
|
rows = json.loads(deliveries.get("body") or "[]")
|
|
except Exception:
|
|
rows = []
|
|
delivery = next((item for item in rows if item.get("event") == "ping"), rows[0] if rows else None)
|
|
payload = {"ok": bool(ping.get("ok") and (delivery is None or delivery.get("status_code") in (200, 202))), "repository": repository, "hookId": hook_id, "pingStatus": ping.get("status"), "delivery": {"id": delivery.get("id"), "event": delivery.get("event"), "statusCode": delivery.get("status_code"), "status": delivery.get("status"), "deliveredAt": delivery.get("delivered_at")} if isinstance(delivery, dict) else None, "error": None if ping.get("ok") else ping.get("body"), "valuesPrinted": False}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
sys.exit(0 if payload["ok"] else 1)
|
|
PY
|
|
ping_rc=$?
|
|
repos_inline=$(cat "$tmp/repos.json")
|
|
kubectl -n "$UNIDESK_GITEA_NAMESPACE" exec -i deploy/"$UNIDESK_GITEA_WEBHOOK_DEPLOYMENT" -- node - "$UNIDESK_GITEA_WEBHOOK_PATH" "$repos_inline" >"$tmp/webhook-test.out" 2>"$tmp/webhook-test.err" <<'NODE'
|
|
const http = require('node:http');
|
|
const crypto = require('node:crypto');
|
|
const path = process.argv[2];
|
|
const repos = JSON.parse(process.argv[3]);
|
|
const repo = repos[0];
|
|
const payload = Buffer.from(JSON.stringify({ ref: `refs/heads/${repo.upstream.branch}`, repository: { full_name: repo.upstream.repository } }));
|
|
const signature = `sha256=${crypto.createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET).update(payload).digest('hex')}`;
|
|
const req = http.request({ hostname: '127.0.0.1', port: Number(process.env.UNIDESK_GITEA_WEBHOOK_PORT || '8080'), path, method: 'POST', headers: { 'content-type': 'application/json', 'content-length': payload.length, 'x-github-event': 'push', 'x-hub-signature-256': signature } }, (res) => {
|
|
const chunks = [];
|
|
res.on('data', (chunk) => chunks.push(chunk));
|
|
res.on('end', () => {
|
|
const body = Buffer.concat(chunks).toString('utf8');
|
|
process.stdout.write(JSON.stringify({ statusCode: res.statusCode, body }) + '\n');
|
|
process.exit(res.statusCode >= 200 && res.statusCode < 300 ? 0 : 1);
|
|
});
|
|
});
|
|
req.on('error', (error) => { console.error(error.stack || error.message); process.exit(1); });
|
|
req.write(payload);
|
|
req.end();
|
|
NODE
|
|
test_rc=$?
|
|
run_mirror_status >"$tmp/status-after-test.json" 2>"$tmp/status-after-test.err"
|
|
status_rc=$?
|
|
python3 - "$ping_rc" "$test_rc" "$status_rc" "$repo_key" "$tmp/repos.json" "$tmp/webhook-ping.json" "$tmp/webhook-ping.err" "$tmp/webhook-test.out" "$tmp/webhook-test.err" "$tmp/status-after-test.json" "$tmp/status-after-test.err" <<'PY'
|
|
import json, sys
|
|
ping_rc, test_rc, status_rc = int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3])
|
|
repo_key = sys.argv[4]
|
|
repos_path = sys.argv[5]
|
|
def text(path, limit=2000):
|
|
try:
|
|
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
|
|
except FileNotFoundError:
|
|
return ""
|
|
def github_head(repo):
|
|
import os, urllib.error, urllib.request
|
|
repository = repo["upstream"]["repository"]
|
|
branch = repo["upstream"]["branch"].replace("/", "%2F")
|
|
token = os.environ["UNIDESK_GITEA_GITHUB_TOKEN"]
|
|
req = urllib.request.Request(f"https://api.github.com/repos/{repository}/git/ref/heads/{branch}", method="GET")
|
|
req.add_header("Authorization", "Bearer " + token)
|
|
req.add_header("Accept", "application/vnd.github+json")
|
|
req.add_header("X-GitHub-Api-Version", "2022-11-28")
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
payload = json.loads(resp.read().decode("utf-8", errors="replace") or "{}")
|
|
return payload.get("object", {}).get("sha")
|
|
except Exception:
|
|
return None
|
|
try:
|
|
ping = json.loads(text(sys.argv[6], 100000))
|
|
except Exception:
|
|
ping = {"ok": False, "error": text(sys.argv[7])}
|
|
try:
|
|
status = json.loads(text(sys.argv[10], 100000))
|
|
except Exception:
|
|
status = {}
|
|
try:
|
|
repo_specs = json.load(open(repos_path, encoding="utf-8"))
|
|
except Exception:
|
|
repo_specs = []
|
|
expected_heads = {repo.get("key"): github_head(repo) for repo in repo_specs}
|
|
rows = []
|
|
for repo in status.get("repositories", []):
|
|
if repo.get("key") == repo_key:
|
|
expected_head = expected_heads.get(repo_key)
|
|
branch_matches_github = bool(expected_head and repo.get("branchCommit") == expected_head)
|
|
rows.append({**repo, "expectedGitHubHead": expected_head, "branchMatchesGitHub": branch_matches_github, "pingOk": ping_rc == 0 and ping.get("ok") is True, "testOk": test_rc == 0, "error": None if ping_rc == 0 and test_rc == 0 and repo.get("sourceBundleReady") and branch_matches_github else (ping.get("error") or text(sys.argv[9]) or text(sys.argv[11]) or "gitea mirror branch does not match GitHub head")[:500]})
|
|
payload = {"ok": ping_rc == 0 and test_rc == 0 and status_rc == 0 and all(row.get("sourceBundleReady") and row.get("branchMatchesGitHub") for row in rows), "repositories": rows, "githubPing": ping, "test": {"exitCode": test_rc, "stdoutTail": text(sys.argv[8]), "stderrTail": text(sys.argv[9])}, "valuesPrinted": False}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
sys.exit(0 if payload["ok"] else 1)
|
|
PY
|
|
}
|
|
|
|
case "$UNIDESK_GITEA_ACTION" in
|
|
apply) run_apply ;;
|
|
status) run_status ;;
|
|
validate) run_validate ;;
|
|
mirror-bootstrap) run_mirror_bootstrap ;;
|
|
mirror-sync) run_mirror_sync ;;
|
|
mirror-status) run_mirror_status ;;
|
|
mirror-webhook-apply) run_mirror_webhook_apply ;;
|
|
mirror-webhook-status) run_mirror_webhook_status ;;
|
|
mirror-webhook-test) run_mirror_webhook_test ;;
|
|
*) printf '{"ok":false,"error":"unsupported-gitea-remote-action"}\n'; exit 2 ;;
|
|
esac
|