# shuriken-sync: a SEPARATE CronJob that publishes the generated # /data/shuriken.sh//dist trees to the public web servers (fishfinger + # blowfish) over the rsync DAEMON protocol (no SSH, no key). The frontends run # rsyncd via inetd with `hosts allow = *.wg0.wan.buetow.org,*.wg0,localhost`; the # k3s pods run on r-nodes that have .wg0 (WireGuard) connectivity, so they match # and can push over the mesh. The writable modules `irregular-ninja` and # `alt-irregular-ninja` are declared in frontends/etc/rsyncd.conf.tpl (deploy # with `rex -f frontends/Rexfile rsync`). # # It only publishes a site when its content actually changed: each site's # dist/status.json (image_count + total_size_bytes) is compared against a # backup copy of the status.json from the last publish # (/.last-published-status.json on NFS -- the persisted "did it # change" state). generated_at is excluded from the comparison because # shuriken rewrites it on every run regardless of whether the source images # changed, so comparing the whole file would publish every tick. The shuriken # `--sync` over SSH stays available as an option; this cron job just uses the # rsync protocol instead. # # The nfs-check initContainer refuses to start if NFS is down on the node, so # rsync --delete can never run against an empty/stale source and wipe the live # public site. apiVersion: batch/v1 kind: CronJob metadata: name: shuriken-sync namespace: services spec: # Twice a day, during daytime hours, well clear of the 04:00 generation # run (avoids the flock race the two CronJobs used to hit when a tick # landed on top of a still-running generation). Each tick is a cheap # per-site status.json comparison; only sites that actually changed # since the last publish get rsynced. schedule: "0 10,18 * * *" timeZone: Europe/Sofia concurrencyPolicy: Forbid startingDeadlineSeconds: 300 successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 3 jobTemplate: spec: backoffLimit: 0 activeDeadlineSeconds: 3600 template: spec: restartPolicy: Never hostAliases: # The frontends are reached over the WireGuard mesh (.wg0); cluster # DNS does not resolve *.wg0 names, so pin them to the mesh IPs. - ip: 192.168.2.111 hostnames: - fishfinger.wg0 - fishfinger.wg0.wan.buetow.org - ip: 192.168.2.110 hostnames: - blowfish.wg0 - blowfish.wg0.wan.buetow.org initContainers: - name: nfs-check image: busybox:stable command: - sh - -c - | test -f /mnt/shuriken.sh/.nfs-sentinel || ( echo "ERROR: NFS sentinel missing at /mnt/shuriken.sh/.nfs-sentinel" echo "refusing to publish; NFS likely unmounted on this node" echo "rsync --delete against an empty/stale source would wipe the live site" exit 1 ) volumeMounts: - name: data mountPath: /mnt readOnly: true containers: - name: shuriken-sync image: registry.lan.buetow.org:30001/shuriken:0.14.0 imagePullPolicy: Always command: ["/bin/bash", "-c"] args: - | set -euo pipefail D=/data/shuriken.sh # Mutually exclude with the generation CronJob: don't publish # while a generation is running. Non-blocking -- if the lock is # held (generation in progress), skip this tick and let the next # one publish once generation finishes. flock auto-releases if # the generation pod dies, so a crash never wedges the sync. exec 9>"$D/.lock" if ! flock -n 9; then echo "shuriken-sync: generation in progress (lock held); skipping" exit 0 fi # Pull "image_count" and "total_size_bytes" out of a shuriken # status.json without a JSON parser (the image ships grep/sed, # not jq). The sidecar's layout is fixed # (status-metadata.source.sh in shuriken.sh), so grep+digits # is safe. generated_at is deliberately NOT part of the # fingerprint: it's rewritten on every run regardless of # whether the source images changed, so including it would # make every tick look "changed". status_fingerprint() { grep '"image_count"' "$1" | grep -oE '[0-9]+' grep '"total_size_bytes"' "$1" | grep -oE '[0-9]+' } published=0 for pair in irregular.ninja:irregular-ninja alt.irregular.ninja:alt-irregular-ninja; do site=${pair%%:*} mod=${pair##*:} status="$D/$site/dist/status.json" backup="$D/$site/.last-published-status.json" if [ ! -f "$status" ]; then echo "shuriken-sync: $status absent (generation in progress or failed); skipping $site" continue fi cur=$(status_fingerprint "$status") prev="" [ -f "$backup" ] && prev=$(status_fingerprint "$backup") if [ "$cur" = "$prev" ]; then echo "shuriken-sync: $site unchanged since last publish; skipping" continue fi echo "shuriken-sync: $site changed since last publish; publishing" for srv in fishfinger.wg0 blowfish.wg0; do echo "shuriken-sync: $site -> rsync://$srv/$mod/" rsync -a --delete --info=stats1 \ "$D/$site/dist/" "rsync://$srv/$mod/" done cp "$status" "$backup" published=1 done if [ "$published" -eq 0 ]; then echo "shuriken-sync: nothing changed; no sites published" else echo "shuriken-sync: done" fi resources: requests: cpu: 50m memory: 64Mi limits: cpu: "1" memory: 512Mi securityContext: allowPrivilegeEscalation: false runAsUser: 0 runAsGroup: 0 volumeMounts: - name: data mountPath: /data - name: tmp mountPath: /tmp volumes: - name: data persistentVolumeClaim: claimName: shuriken-data-pvc - name: tmp emptyDir: sizeLimit: 256Mi